diff --git a/agent/app/api/v1/app.go b/agent/app/api/v1/app.go new file mode 100644 index 000000000..c07cc3cfb --- /dev/null +++ b/agent/app/api/v1/app.go @@ -0,0 +1,197 @@ +package v1 + +import ( + "github.com/1Panel-dev/1Panel/agent/app/api/v1/helper" + "github.com/1Panel-dev/1Panel/agent/app/dto/request" + "github.com/1Panel-dev/1Panel/agent/constant" + "github.com/1Panel-dev/1Panel/agent/global" + "github.com/1Panel-dev/1Panel/agent/i18n" + "github.com/gin-gonic/gin" +) + +// @Tags App +// @Summary List apps +// @Description 获取应用列表 +// @Accept json +// @Param request body request.AppSearch true "request" +// @Success 200 +// @Security ApiKeyAuth +// @Router /apps/search [post] +func (b *BaseApi) SearchApp(c *gin.Context) { + var req request.AppSearch + if err := helper.CheckBindAndValidate(&req, c); err != nil { + return + } + list, err := appService.PageApp(req) + if err != nil { + helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) + return + } + helper.SuccessWithData(c, list) +} + +// @Tags App +// @Summary Sync app list +// @Description 同步应用列表 +// @Success 200 +// @Security ApiKeyAuth +// @Router /apps/sync [post] +// @x-panel-log {"bodyKeys":[],"paramKeys":[],"BeforeFunctions":[],"formatZH":"应用商店同步","formatEN":"App store synchronization"} +func (b *BaseApi) SyncApp(c *gin.Context) { + go appService.SyncAppListFromLocal() + res, err := appService.GetAppUpdate() + if err != nil { + helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) + return + } + + if !res.CanUpdate { + if res.IsSyncing { + helper.SuccessWithMsg(c, i18n.GetMsgByKey("AppStoreIsSyncing")) + } else { + helper.SuccessWithMsg(c, i18n.GetMsgByKey("AppStoreIsUpToDate")) + } + return + } + go func() { + if err := appService.SyncAppListFromRemote(); err != nil { + global.LOG.Errorf("Synchronization with the App Store failed [%s]", err.Error()) + } + }() + helper.SuccessWithData(c, "") +} + +// @Tags App +// @Summary Search app by key +// @Description 通过 key 获取应用信息 +// @Accept json +// @Param key path string true "app key" +// @Success 200 {object} response.AppDTO +// @Security ApiKeyAuth +// @Router /apps/:key [get] +func (b *BaseApi) GetApp(c *gin.Context) { + appKey, err := helper.GetStrParamByKey(c, "key") + if err != nil { + helper.ErrorWithDetail(c, constant.CodeErrBadRequest, constant.ErrTypeInvalidParams, err) + return + } + appDTO, err := appService.GetApp(appKey) + if err != nil { + helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) + return + } + helper.SuccessWithData(c, appDTO) +} + +// @Tags App +// @Summary Search app detail by appid +// @Description 通过 appid 获取应用详情 +// @Accept json +// @Param appId path integer true "app id" +// @Param version path string true "app 版本" +// @Param version path string true "app 类型" +// @Success 200 {object} response.AppDetailDTO +// @Security ApiKeyAuth +// @Router /apps/detail/:appId/:version/:type [get] +func (b *BaseApi) GetAppDetail(c *gin.Context) { + appID, err := helper.GetIntParamByKey(c, "appId") + if err != nil { + helper.ErrorWithDetail(c, constant.CodeErrBadRequest, constant.ErrTypeInternalServer, nil) + return + } + version := c.Param("version") + appType := c.Param("type") + appDetailDTO, err := appService.GetAppDetail(appID, version, appType) + if err != nil { + helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) + return + } + helper.SuccessWithData(c, appDetailDTO) +} + +// @Tags App +// @Summary Get app detail by id +// @Description 通过 id 获取应用详情 +// @Accept json +// @Param appId path integer true "id" +// @Success 200 {object} response.AppDetailDTO +// @Security ApiKeyAuth +// @Router /apps/details/:id [get] +func (b *BaseApi) GetAppDetailByID(c *gin.Context) { + appDetailID, err := helper.GetIntParamByKey(c, "id") + if err != nil { + helper.ErrorWithDetail(c, constant.CodeErrBadRequest, constant.ErrTypeInternalServer, nil) + return + } + appDetailDTO, err := appService.GetAppDetailByID(appDetailID) + if err != nil { + helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) + return + } + helper.SuccessWithData(c, appDetailDTO) +} + +// @Tags App +// @Summary Get Ignore App +// @Description 获取忽略的应用版本 +// @Accept json +// @Success 200 {object} response.IgnoredApp +// @Security ApiKeyAuth +// @Router /apps/ignored [get] +func (b *BaseApi) GetIgnoredApp(c *gin.Context) { + res, err := appService.GetIgnoredApp() + if err != nil { + helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) + return + } + helper.SuccessWithData(c, res) +} + +// @Tags App +// @Summary Install app +// @Description 安装应用 +// @Accept json +// @Param request body request.AppInstallCreate true "request" +// @Success 200 {object} model.AppInstall +// @Security ApiKeyAuth +// @Router /apps/install [post] +// @x-panel-log {"bodyKeys":["name"],"paramKeys":[],"BeforeFunctions":[{"input_column":"name","input_value":"name","isList":false,"db":"app_installs","output_column":"app_id","output_value":"appId"},{"info":"appId","isList":false,"db":"apps","output_column":"key","output_value":"appKey"}],"formatZH":"安装应用 [appKey]-[name]","formatEN":"Install app [appKey]-[name]"} +func (b *BaseApi) InstallApp(c *gin.Context) { + var req request.AppInstallCreate + if err := helper.CheckBindAndValidate(&req, c); err != nil { + return + } + tx, ctx := helper.GetTxAndContext() + install, err := appService.Install(ctx, req) + if err != nil { + tx.Rollback() + helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) + return + } + tx.Commit() + helper.SuccessWithData(c, install) +} + +func (b *BaseApi) GetAppTags(c *gin.Context) { + tags, err := appService.GetAppTags() + if err != nil { + helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) + return + } + helper.SuccessWithData(c, tags) +} + +// @Tags App +// @Summary Get app list update +// @Description 获取应用更新版本 +// @Success 200 +// @Security ApiKeyAuth +// @Router /apps/checkupdate [get] +func (b *BaseApi) GetAppListUpdate(c *gin.Context) { + res, err := appService.GetAppUpdate() + if err != nil { + helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) + return + } + helper.SuccessWithData(c, res) +} diff --git a/agent/app/api/v1/app_install.go b/agent/app/api/v1/app_install.go new file mode 100644 index 000000000..4429c5d72 --- /dev/null +++ b/agent/app/api/v1/app_install.go @@ -0,0 +1,327 @@ +package v1 + +import ( + "github.com/1Panel-dev/1Panel/agent/app/dto/request" + + "github.com/1Panel-dev/1Panel/agent/app/api/v1/helper" + "github.com/1Panel-dev/1Panel/agent/app/dto" + "github.com/1Panel-dev/1Panel/agent/constant" + "github.com/gin-gonic/gin" +) + +// @Tags App +// @Summary Page app installed +// @Description 分页获取已安装应用列表 +// @Accept json +// @Param request body request.AppInstalledSearch true "request" +// @Success 200 +// @Security ApiKeyAuth +// @Router /apps/installed/search [post] +func (b *BaseApi) SearchAppInstalled(c *gin.Context) { + var req request.AppInstalledSearch + if err := helper.CheckBindAndValidate(&req, c); err != nil { + return + } + if req.All { + list, err := appInstallService.SearchForWebsite(req) + if err != nil { + helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) + return + } + helper.SuccessWithData(c, list) + } else { + total, list, err := appInstallService.Page(req) + if err != nil { + helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) + return + } + helper.SuccessWithData(c, dto.PageResult{ + Items: list, + Total: total, + }) + } +} + +// @Tags App +// @Summary List app installed +// @Description 获取已安装应用列表 +// @Accept json +// @Success 200 array dto.AppInstallInfo +// @Security ApiKeyAuth +// @Router /apps/installed/list [get] +func (b *BaseApi) ListAppInstalled(c *gin.Context) { + list, err := appInstallService.GetInstallList() + if err != nil { + helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) + return + } + helper.SuccessWithData(c, list) +} + +// @Tags App +// @Summary Check app installed +// @Description 检查应用安装情况 +// @Accept json +// @Param request body request.AppInstalledInfo true "request" +// @Success 200 {object} response.AppInstalledCheck +// @Security ApiKeyAuth +// @Router /apps/installed/check [post] +func (b *BaseApi) CheckAppInstalled(c *gin.Context) { + var req request.AppInstalledInfo + if err := helper.CheckBindAndValidate(&req, c); err != nil { + return + } + checkData, err := appInstallService.CheckExist(req) + if err != nil { + helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) + return + } + helper.SuccessWithData(c, checkData) +} + +// @Tags App +// @Summary Search app port by key +// @Description 获取应用端口 +// @Accept json +// @Param request body dto.OperationWithNameAndType true "request" +// @Success 200 {integer} port +// @Security ApiKeyAuth +// @Router /apps/installed/loadport [post] +func (b *BaseApi) LoadPort(c *gin.Context) { + var req dto.OperationWithNameAndType + if err := helper.CheckBindAndValidate(&req, c); err != nil { + return + } + port, err := appInstallService.LoadPort(req) + if err != nil { + helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) + return + } + helper.SuccessWithData(c, port) +} + +// @Tags App +// @Summary Search app password by key +// @Description 获取应用连接信息 +// @Accept json +// @Param request body dto.OperationWithNameAndType true "request" +// @Success 200 {string} response.DatabaseConn +// @Security ApiKeyAuth +// @Router /apps/installed/conninfo/:key [get] +func (b *BaseApi) LoadConnInfo(c *gin.Context) { + var req dto.OperationWithNameAndType + if err := helper.CheckBindAndValidate(&req, c); err != nil { + return + } + conn, err := appInstallService.LoadConnInfo(req) + if err != nil { + helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) + return + } + helper.SuccessWithData(c, conn) +} + +// @Tags App +// @Summary Check before delete +// @Description 删除前检查 +// @Accept json +// @Param appInstallId path integer true "App install id" +// @Success 200 {array} dto.AppResource +// @Security ApiKeyAuth +// @Router /apps/installed/delete/check/:appInstallId [get] +func (b *BaseApi) DeleteCheck(c *gin.Context) { + appInstallId, err := helper.GetIntParamByKey(c, "appInstallId") + if err != nil { + helper.ErrorWithDetail(c, constant.CodeErrBadRequest, constant.ErrTypeInternalServer, nil) + return + } + checkData, err := appInstallService.DeleteCheck(appInstallId) + if err != nil { + helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) + return + } + helper.SuccessWithData(c, checkData) +} + +// Sync app installed +// @Tags App +// @Summary Sync app installed +// @Description 同步已安装应用列表 +// @Success 200 +// @Security ApiKeyAuth +// @Router /apps/installed/sync [post] +// @x-panel-log {"bodyKeys":[],"paramKeys":[],"BeforeFunctions":[],"formatZH":"同步已安装应用列表","formatEN":"Sync the list of installed apps"} +func (b *BaseApi) SyncInstalled(c *gin.Context) { + if err := appInstallService.SyncAll(false); err != nil { + helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) + return + } + helper.SuccessWithData(c, "") +} + +// @Tags App +// @Summary Operate installed app +// @Description 操作已安装应用 +// @Accept json +// @Param request body request.AppInstalledOperate true "request" +// @Success 200 +// @Security ApiKeyAuth +// @Router /apps/installed/op [post] +// @x-panel-log {"bodyKeys":["installId","operate"],"paramKeys":[],"BeforeFunctions":[{"input_column":"id","input_value":"installId","isList":false,"db":"app_installs","output_column":"app_id","output_value":"appId"},{"input_column":"id","input_value":"installId","isList":false,"db":"app_installs","output_column":"name","output_value":"appName"},{"input_column":"id","input_value":"appId","isList":false,"db":"apps","output_column":"key","output_value":"appKey"}],"formatZH":"[operate] 应用 [appKey][appName]","formatEN":"[operate] App [appKey][appName]"} +func (b *BaseApi) OperateInstalled(c *gin.Context) { + var req request.AppInstalledOperate + if err := helper.CheckBindAndValidate(&req, c); err != nil { + return + } + if err := appInstallService.Operate(req); err != nil { + helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) + return + } + helper.SuccessWithData(c, nil) +} + +// @Tags App +// @Summary Search app service by key +// @Description 通过 key 获取应用 service +// @Accept json +// @Param key path string true "request" +// @Success 200 {array} response.AppService +// @Security ApiKeyAuth +// @Router /apps/services/:key [get] +func (b *BaseApi) GetServices(c *gin.Context) { + key := c.Param("key") + services, err := appInstallService.GetServices(key) + if err != nil { + helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) + return + } + helper.SuccessWithData(c, services) +} + +// @Tags App +// @Summary Search app update version by install id +// @Description 通过 install id 获取应用更新版本 +// @Accept json +// @Param appInstallId path integer true "request" +// @Success 200 {array} dto.AppVersion +// @Security ApiKeyAuth +// @Router /apps/installed/update/versions [post] +func (b *BaseApi) GetUpdateVersions(c *gin.Context) { + var req request.AppUpdateVersion + if err := helper.CheckBindAndValidate(&req, c); err != nil { + return + } + versions, err := appInstallService.GetUpdateVersions(req) + if err != nil { + helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) + return + } + helper.SuccessWithData(c, versions) +} + +// @Tags App +// @Summary Change app port +// @Description 修改应用端口 +// @Accept json +// @Param request body request.PortUpdate true "request" +// @Success 200 +// @Security ApiKeyAuth +// @Router /apps/installed/port/change [post] +// @x-panel-log {"bodyKeys":["key","name","port"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"应用端口修改 [key]-[name] => [port]","formatEN":"Application port update [key]-[name] => [port]"} +func (b *BaseApi) ChangeAppPort(c *gin.Context) { + var req request.PortUpdate + if err := helper.CheckBindAndValidate(&req, c); err != nil { + return + } + if err := appInstallService.ChangeAppPort(req); err != nil { + helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) + return + } + helper.SuccessWithData(c, nil) +} + +// @Tags App +// @Summary Search default config by key +// @Description 通过 key 获取应用默认配置 +// @Accept json +// @Param request body dto.OperationWithNameAndType true "request" +// @Success 200 {string} content +// @Security ApiKeyAuth +// @Router /apps/installed/conf [post] +func (b *BaseApi) GetDefaultConfig(c *gin.Context) { + var req dto.OperationWithNameAndType + if err := helper.CheckBindAndValidate(&req, c); err != nil { + return + } + content, err := appInstallService.GetDefaultConfigByKey(req.Type, req.Name) + if err != nil { + helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) + return + } + + helper.SuccessWithData(c, content) +} + +// @Tags App +// @Summary Search params by appInstallId +// @Description 通过 install id 获取应用参数 +// @Accept json +// @Param appInstallId path string true "request" +// @Success 200 {object} response.AppParam +// @Security ApiKeyAuth +// @Router /apps/installed/params/:appInstallId [get] +func (b *BaseApi) GetParams(c *gin.Context) { + appInstallId, err := helper.GetIntParamByKey(c, "appInstallId") + if err != nil { + helper.ErrorWithDetail(c, constant.CodeErrBadRequest, constant.ErrTypeInternalServer, nil) + return + } + content, err := appInstallService.GetParams(appInstallId) + if err != nil { + helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) + return + } + helper.SuccessWithData(c, content) +} + +// @Tags App +// @Summary Change app params +// @Description 修改应用参数 +// @Accept json +// @Param request body request.AppInstalledUpdate true "request" +// @Success 200 +// @Security ApiKeyAuth +// @Router /apps/installed/params/update [post] +// @x-panel-log {"bodyKeys":["installId"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"应用参数修改 [installId]","formatEN":"Application param update [installId]"} +func (b *BaseApi) UpdateInstalled(c *gin.Context) { + var req request.AppInstalledUpdate + if err := helper.CheckBindAndValidate(&req, c); err != nil { + return + } + if err := appInstallService.Update(req); err != nil { + helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) + return + } + helper.SuccessWithData(c, nil) +} + +// @Tags App +// @Summary ignore App Update +// @Description 忽略应用升级版本 +// @Accept json +// @Param request body request.AppInstalledIgnoreUpgrade true "request" +// @Success 200 +// @Security ApiKeyAuth +// @Router /apps/installed/ignore [post] +// @x-panel-log {"bodyKeys":["installId"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"忽略应用 [installId] 版本升级","formatEN":"Application param update [installId]"} +func (b *BaseApi) IgnoreUpgrade(c *gin.Context) { + var req request.AppInstalledIgnoreUpgrade + if err := helper.CheckBindAndValidate(&req, c); err != nil { + return + } + if err := appInstallService.IgnoreUpgrade(req); err != nil { + helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) + return + } + helper.SuccessWithOutData(c) +} diff --git a/agent/app/api/v1/backup.go b/agent/app/api/v1/backup.go new file mode 100644 index 000000000..bb42563da --- /dev/null +++ b/agent/app/api/v1/backup.go @@ -0,0 +1,443 @@ +package v1 + +import ( + "encoding/base64" + "fmt" + "path" + + "github.com/1Panel-dev/1Panel/agent/app/api/v1/helper" + "github.com/1Panel-dev/1Panel/agent/app/dto" + "github.com/1Panel-dev/1Panel/agent/constant" + "github.com/gin-gonic/gin" +) + +// @Tags Backup Account +// @Summary Create backup account +// @Description 创建备份账号 +// @Accept json +// @Param request body dto.BackupOperate true "request" +// @Success 200 +// @Security ApiKeyAuth +// @Router /settings/backup [post] +// @x-panel-log {"bodyKeys":["type"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"创建备份账号 [type]","formatEN":"create backup account [type]"} +func (b *BaseApi) CreateBackup(c *gin.Context) { + var req dto.BackupOperate + if err := helper.CheckBindAndValidate(&req, c); err != nil { + return + } + if len(req.Credential) != 0 { + credential, err := base64.StdEncoding.DecodeString(req.Credential) + if err != nil { + helper.ErrorWithDetail(c, constant.CodeErrBadRequest, constant.ErrTypeInvalidParams, err) + return + } + req.Credential = string(credential) + } + if len(req.AccessKey) != 0 { + accessKey, err := base64.StdEncoding.DecodeString(req.AccessKey) + if err != nil { + helper.ErrorWithDetail(c, constant.CodeErrBadRequest, constant.ErrTypeInvalidParams, err) + return + } + req.AccessKey = string(accessKey) + } + + if err := backupService.Create(req); err != nil { + helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) + return + } + helper.SuccessWithData(c, nil) +} + +// @Tags Backup Account +// @Summary Refresh OneDrive token +// @Description 刷新 OneDrive token +// @Success 200 +// @Security ApiKeyAuth +// @Router /settings/backup/refresh/onedrive [post] +func (b *BaseApi) RefreshOneDriveToken(c *gin.Context) { + backupService.Run() + helper.SuccessWithData(c, nil) +} + +// @Tags Backup Account +// @Summary List buckets +// @Description 获取 bucket 列表 +// @Accept json +// @Param request body dto.ForBuckets true "request" +// @Success 200 {array} string +// @Security ApiKeyAuth +// @Router /settings/backup/search [post] +func (b *BaseApi) ListBuckets(c *gin.Context) { + var req dto.ForBuckets + if err := helper.CheckBindAndValidate(&req, c); err != nil { + return + } + if len(req.Credential) != 0 { + credential, err := base64.StdEncoding.DecodeString(req.Credential) + if err != nil { + helper.ErrorWithDetail(c, constant.CodeErrBadRequest, constant.ErrTypeInvalidParams, err) + return + } + req.Credential = string(credential) + } + if len(req.AccessKey) != 0 { + accessKey, err := base64.StdEncoding.DecodeString(req.AccessKey) + if err != nil { + helper.ErrorWithDetail(c, constant.CodeErrBadRequest, constant.ErrTypeInvalidParams, err) + return + } + req.AccessKey = string(accessKey) + } + + buckets, err := backupService.GetBuckets(req) + if err != nil { + helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) + return + } + helper.SuccessWithData(c, buckets) +} + +// @Tags Backup Account +// @Summary Load OneDrive info +// @Description 获取 OneDrive 信息 +// @Accept json +// @Success 200 {object} dto.OneDriveInfo +// @Security ApiKeyAuth +// @Router /settings/backup/onedrive [get] +func (b *BaseApi) LoadOneDriveInfo(c *gin.Context) { + data, err := backupService.LoadOneDriveInfo() + if err != nil { + helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) + return + } + helper.SuccessWithData(c, data) +} + +// @Tags Backup Account +// @Summary Delete backup account +// @Description 删除备份账号 +// @Accept json +// @Param request body dto.OperateByID true "request" +// @Success 200 +// @Security ApiKeyAuth +// @Router /settings/backup/del [post] +// @x-panel-log {"bodyKeys":["id"],"paramKeys":[],"BeforeFunctions":[{"input_column":"id","input_value":"id","isList":false,"db":"backup_accounts","output_column":"type","output_value":"types"}],"formatZH":"删除备份账号 [types]","formatEN":"delete backup account [types]"} +func (b *BaseApi) DeleteBackup(c *gin.Context) { + var req dto.OperateByID + if err := helper.CheckBindAndValidate(&req, c); err != nil { + return + } + + if err := backupService.Delete(req.ID); err != nil { + helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) + return + } + helper.SuccessWithData(c, nil) +} + +// @Tags Backup Account +// @Summary Page backup records +// @Description 获取备份记录列表分页 +// @Accept json +// @Param request body dto.RecordSearch true "request" +// @Success 200 +// @Security ApiKeyAuth +// @Router /settings/backup/record/search [post] +func (b *BaseApi) SearchBackupRecords(c *gin.Context) { + var req dto.RecordSearch + if err := helper.CheckBindAndValidate(&req, c); err != nil { + return + } + + total, list, err := backupService.SearchRecordsWithPage(req) + if err != nil { + helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) + return + } + + helper.SuccessWithData(c, dto.PageResult{ + Items: list, + Total: total, + }) +} + +// @Tags Backup Account +// @Summary Page backup records by cronjob +// @Description 通过计划任务获取备份记录列表分页 +// @Accept json +// @Param request body dto.RecordSearchByCronjob true "request" +// @Success 200 +// @Security ApiKeyAuth +// @Router /settings/backup/record/search/bycronjob [post] +func (b *BaseApi) SearchBackupRecordsByCronjob(c *gin.Context) { + var req dto.RecordSearchByCronjob + if err := helper.CheckBindAndValidate(&req, c); err != nil { + return + } + + total, list, err := backupService.SearchRecordsByCronjobWithPage(req) + if err != nil { + helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) + return + } + + helper.SuccessWithData(c, dto.PageResult{ + Items: list, + Total: total, + }) +} + +// @Tags Backup Account +// @Summary Download backup record +// @Description 下载备份记录 +// @Accept json +// @Param request body dto.DownloadRecord true "request" +// @Success 200 +// @Security ApiKeyAuth +// @Router /settings/backup/record/download [post] +// @x-panel-log {"bodyKeys":["source","fileName"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"下载备份记录 [source][fileName]","formatEN":"download backup records [source][fileName]"} +func (b *BaseApi) DownloadRecord(c *gin.Context) { + var req dto.DownloadRecord + if err := helper.CheckBindAndValidate(&req, c); err != nil { + return + } + + filePath, err := backupService.DownloadRecord(req) + if err != nil { + helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) + return + } + helper.SuccessWithData(c, filePath) +} + +// @Tags Backup Account +// @Summary Delete backup record +// @Description 删除备份记录 +// @Accept json +// @Param request body dto.BatchDeleteReq true "request" +// @Success 200 +// @Security ApiKeyAuth +// @Router /settings/backup/record/del [post] +// @x-panel-log {"bodyKeys":["ids"],"paramKeys":[],"BeforeFunctions":[{"input_column":"id","input_value":"ids","isList":true,"db":"backup_records","output_column":"file_name","output_value":"files"}],"formatZH":"删除备份记录 [files]","formatEN":"delete backup records [files]"} +func (b *BaseApi) DeleteBackupRecord(c *gin.Context) { + var req dto.BatchDeleteReq + if err := helper.CheckBindAndValidate(&req, c); err != nil { + return + } + + if err := backupService.BatchDeleteRecord(req.Ids); err != nil { + helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) + return + } + helper.SuccessWithData(c, nil) +} + +// @Tags Backup Account +// @Summary Update backup account +// @Description 更新备份账号信息 +// @Accept json +// @Param request body dto.BackupOperate true "request" +// @Success 200 +// @Security ApiKeyAuth +// @Router /settings/backup/update [post] +// @x-panel-log {"bodyKeys":["type"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"更新备份账号 [types]","formatEN":"update backup account [types]"} +func (b *BaseApi) UpdateBackup(c *gin.Context) { + var req dto.BackupOperate + if err := helper.CheckBindAndValidate(&req, c); err != nil { + return + } + + if len(req.Credential) != 0 { + credential, err := base64.StdEncoding.DecodeString(req.Credential) + if err != nil { + helper.ErrorWithDetail(c, constant.CodeErrBadRequest, constant.ErrTypeInvalidParams, err) + return + } + req.Credential = string(credential) + } + if len(req.AccessKey) != 0 { + accessKey, err := base64.StdEncoding.DecodeString(req.AccessKey) + if err != nil { + helper.ErrorWithDetail(c, constant.CodeErrBadRequest, constant.ErrTypeInvalidParams, err) + return + } + req.AccessKey = string(accessKey) + } + + if err := backupService.Update(req); err != nil { + helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) + return + } + helper.SuccessWithData(c, nil) +} + +// @Tags Backup Account +// @Summary List backup accounts +// @Description 获取备份账号列表 +// @Success 200 {array} dto.BackupInfo +// @Security ApiKeyAuth +// @Router /settings/backup/search [get] +func (b *BaseApi) ListBackup(c *gin.Context) { + data, err := backupService.List() + if err != nil { + helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) + return + } + + helper.SuccessWithData(c, data) +} + +// @Tags Backup Account +// @Summary List files from backup accounts +// @Description 获取备份账号内文件列表 +// @Accept json +// @Param request body dto.BackupSearchFile true "request" +// @Success 200 {array} string +// @Security ApiKeyAuth +// @Router /settings/backup/search/files [post] +func (b *BaseApi) LoadFilesFromBackup(c *gin.Context) { + var req dto.BackupSearchFile + if err := helper.CheckBindAndValidate(&req, c); err != nil { + return + } + + data := backupService.ListFiles(req) + helper.SuccessWithData(c, data) +} + +// @Tags Backup Account +// @Summary Backup system data +// @Description 备份系统数据 +// @Accept json +// @Param request body dto.CommonBackup true "request" +// @Success 200 +// @Security ApiKeyAuth +// @Router /settings/backup/backup [post] +// @x-panel-log {"bodyKeys":["type","name","detailName"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"备份 [type] 数据 [name][detailName]","formatEN":"backup [type] data [name][detailName]"} +func (b *BaseApi) Backup(c *gin.Context) { + var req dto.CommonBackup + if err := helper.CheckBindAndValidate(&req, c); err != nil { + return + } + + switch req.Type { + case "app": + if _, err := backupService.AppBackup(req); err != nil { + helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) + return + } + case "mysql", "mariadb": + if err := backupService.MysqlBackup(req); err != nil { + helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) + return + } + case constant.AppPostgresql: + if err := backupService.PostgresqlBackup(req); err != nil { + helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) + return + } + case "website": + if err := backupService.WebsiteBackup(req); err != nil { + helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) + return + } + case "redis": + if err := backupService.RedisBackup(req); err != nil { + helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) + return + } + } + helper.SuccessWithData(c, nil) +} + +// @Tags Backup Account +// @Summary Recover system data +// @Description 恢复系统数据 +// @Accept json +// @Param request body dto.CommonRecover true "request" +// @Success 200 +// @Security ApiKeyAuth +// @Router /settings/backup/recover [post] +// @x-panel-log {"bodyKeys":["type","name","detailName","file"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"从 [file] 恢复 [type] 数据 [name][detailName]","formatEN":"recover [type] data [name][detailName] from [file]"} +func (b *BaseApi) Recover(c *gin.Context) { + var req dto.CommonRecover + if err := helper.CheckBindAndValidate(&req, c); err != nil { + return + } + + downloadPath, err := backupService.DownloadRecord(dto.DownloadRecord{Source: req.Source, FileDir: path.Dir(req.File), FileName: path.Base(req.File)}) + if err != nil { + helper.ErrorWithDetail(c, constant.CodeErrBadRequest, constant.ErrTypeInvalidParams, fmt.Errorf("download file failed, err: %v", err)) + return + } + req.File = downloadPath + switch req.Type { + case "mysql", "mariadb": + if err := backupService.MysqlRecover(req); err != nil { + helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) + return + } + case constant.AppPostgresql: + if err := backupService.PostgresqlRecover(req); err != nil { + helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) + return + } + case "website": + if err := backupService.WebsiteRecover(req); err != nil { + helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) + return + } + case "redis": + if err := backupService.RedisRecover(req); err != nil { + helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) + return + } + case "app": + if err := backupService.AppRecover(req); err != nil { + helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) + return + } + } + helper.SuccessWithData(c, nil) +} + +// @Tags Backup Account +// @Summary Recover system data by upload +// @Description 从上传恢复系统数据 +// @Accept json +// @Param request body dto.CommonRecover true "request" +// @Success 200 +// @Security ApiKeyAuth +// @Router /settings/backup/recover/byupload [post] +// @x-panel-log {"bodyKeys":["type","name","detailName","file"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"从 [file] 恢复 [type] 数据 [name][detailName]","formatEN":"recover [type] data [name][detailName] from [file]"} +func (b *BaseApi) RecoverByUpload(c *gin.Context) { + var req dto.CommonRecover + if err := helper.CheckBindAndValidate(&req, c); err != nil { + return + } + + switch req.Type { + case "mysql", "mariadb": + if err := backupService.MysqlRecoverByUpload(req); err != nil { + helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) + return + } + case constant.AppPostgresql: + if err := backupService.PostgresqlRecoverByUpload(req); err != nil { + helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) + return + } + case "app": + if err := backupService.AppRecover(req); err != nil { + helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) + return + } + case "website": + if err := backupService.WebsiteRecover(req); err != nil { + helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) + return + } + } + helper.SuccessWithData(c, nil) +} diff --git a/agent/app/api/v1/clam.go b/agent/app/api/v1/clam.go new file mode 100644 index 000000000..936894868 --- /dev/null +++ b/agent/app/api/v1/clam.go @@ -0,0 +1,296 @@ +package v1 + +import ( + "github.com/1Panel-dev/1Panel/agent/app/api/v1/helper" + "github.com/1Panel-dev/1Panel/agent/app/dto" + "github.com/1Panel-dev/1Panel/agent/constant" + "github.com/gin-gonic/gin" +) + +// @Tags Clam +// @Summary Create clam +// @Description 创建扫描规则 +// @Accept json +// @Param request body dto.ClamCreate true "request" +// @Success 200 +// @Security ApiKeyAuth +// @Router /toolbox/clam [post] +// @x-panel-log {"bodyKeys":["name","path"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"创建扫描规则 [name][path]","formatEN":"create clam [name][path]"} +func (b *BaseApi) CreateClam(c *gin.Context) { + var req dto.ClamCreate + if err := helper.CheckBindAndValidate(&req, c); err != nil { + return + } + + if err := clamService.Create(req); err != nil { + helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) + return + } + helper.SuccessWithData(c, nil) +} + +// @Tags Clam +// @Summary Update clam +// @Description 修改扫描规则 +// @Accept json +// @Param request body dto.ClamUpdate true "request" +// @Success 200 +// @Security ApiKeyAuth +// @Router /toolbox/clam/update [post] +// @x-panel-log {"bodyKeys":["name","path"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"修改扫描规则 [name][path]","formatEN":"update clam [name][path]"} +func (b *BaseApi) UpdateClam(c *gin.Context) { + var req dto.ClamUpdate + if err := helper.CheckBindAndValidate(&req, c); err != nil { + return + } + + if err := clamService.Update(req); err != nil { + helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) + return + } + helper.SuccessWithData(c, nil) +} + +// @Tags Clam +// @Summary Update clam status +// @Description 修改扫描规则状态 +// @Accept json +// @Param request body dto.ClamUpdateStatus true "request" +// @Success 200 +// @Security ApiKeyAuth +// @Router /toolbox/clam/status/update [post] +// @x-panel-log {"bodyKeys":["id","status"],"paramKeys":[],"BeforeFunctions":[{"input_column":"id","input_value":"id","isList":false,"db":"clams","output_column":"name","output_value":"name"}],"formatZH":"修改扫描规则 [name] 状态为 [status]","formatEN":"change the status of clam [name] to [status]."} +func (b *BaseApi) UpdateClamStatus(c *gin.Context) { + var req dto.ClamUpdateStatus + if err := helper.CheckBindAndValidate(&req, c); err != nil { + return + } + + if err := clamService.UpdateStatus(req.ID, req.Status); err != nil { + helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) + return + } + helper.SuccessWithData(c, nil) +} + +// @Tags Clam +// @Summary Page clam +// @Description 获取扫描规则列表分页 +// @Accept json +// @Param request body dto.SearchClamWithPage true "request" +// @Success 200 {object} dto.PageResult +// @Security ApiKeyAuth +// @Router /toolbox/clam/search [post] +func (b *BaseApi) SearchClam(c *gin.Context) { + var req dto.SearchClamWithPage + if err := helper.CheckBindAndValidate(&req, c); err != nil { + return + } + + total, list, err := clamService.SearchWithPage(req) + if err != nil { + helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) + return + } + + helper.SuccessWithData(c, dto.PageResult{ + Items: list, + Total: total, + }) +} + +// @Tags Clam +// @Summary Load clam base info +// @Description 获取 Clam 基础信息 +// @Accept json +// @Success 200 {object} dto.ClamBaseInfo +// @Security ApiKeyAuth +// @Router /toolbox/clam/base [get] +func (b *BaseApi) LoadClamBaseInfo(c *gin.Context) { + info, err := clamService.LoadBaseInfo() + if err != nil { + helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) + return + } + + helper.SuccessWithData(c, info) +} + +// @Tags Clam +// @Summary Operate Clam +// @Description 修改 Clam 状态 +// @Accept json +// @Param request body dto.Operate true "request" +// @Security ApiKeyAuth +// @Router /toolbox/clam/operate [post] +// @x-panel-log {"bodyKeys":["operation"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"[operation] Clam","formatEN":"[operation] FTP"} +func (b *BaseApi) OperateClam(c *gin.Context) { + var req dto.Operate + if err := helper.CheckBindAndValidate(&req, c); err != nil { + return + } + + if err := clamService.Operate(req.Operation); err != nil { + helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) + return + } + + helper.SuccessWithData(c, nil) +} + +// @Tags Clam +// @Summary Clean clam record +// @Description 清空扫描报告 +// @Accept json +// @Param request body dto.OperateByID true "request" +// @Security ApiKeyAuth +// @Router /toolbox/clam/record/clean [post] +// @x-panel-log {"bodyKeys":["id"],"paramKeys":[],"BeforeFunctions":[{"input_column":"id","input_value":"id","isList":true,"db":"clams","output_column":"name","output_value":"name"}],"formatZH":"清空扫描报告 [name]","formatEN":"clean clam record [name]"} +func (b *BaseApi) CleanClamRecord(c *gin.Context) { + var req dto.OperateByID + if err := helper.CheckBindAndValidate(&req, c); err != nil { + return + } + if err := clamService.CleanRecord(req); err != nil { + helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) + return + } + + helper.SuccessWithData(c, nil) +} + +// @Tags Clam +// @Summary Page clam record +// @Description 获取扫描结果列表分页 +// @Accept json +// @Param request body dto.ClamLogSearch true "request" +// @Success 200 {object} dto.PageResult +// @Security ApiKeyAuth +// @Router /toolbox/clam/record/search [post] +func (b *BaseApi) SearchClamRecord(c *gin.Context) { + var req dto.ClamLogSearch + if err := helper.CheckBindAndValidate(&req, c); err != nil { + return + } + + total, list, err := clamService.LoadRecords(req) + if err != nil { + helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) + return + } + + helper.SuccessWithData(c, dto.PageResult{ + Items: list, + Total: total, + }) +} + +// @Tags Clam +// @Summary Load clam record detail +// @Description 获取扫描结果详情 +// @Accept json +// @Param request body dto.ClamLogReq true "request" +// @Success 200 +// @Security ApiKeyAuth +// @Router /toolbox/clam/record/log [post] +func (b *BaseApi) LoadClamRecordLog(c *gin.Context) { + var req dto.ClamLogReq + if err := helper.CheckBindAndValidate(&req, c); err != nil { + return + } + + content, err := clamService.LoadRecordLog(req) + if err != nil { + helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) + return + } + + helper.SuccessWithData(c, content) +} + +// @Tags Clam +// @Summary Load clam file +// @Description 获取扫描文件 +// @Accept json +// @Param request body dto.ClamFileReq true "request" +// @Success 200 {object} dto.PageResult +// @Security ApiKeyAuth +// @Router /toolbox/clam/file/search [post] +func (b *BaseApi) SearchClamFile(c *gin.Context) { + var req dto.ClamFileReq + if err := helper.CheckBindAndValidate(&req, c); err != nil { + return + } + + content, err := clamService.LoadFile(req) + if err != nil { + helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) + return + } + + helper.SuccessWithData(c, content) +} + +// @Tags Clam +// @Summary Update clam file +// @Description 更新病毒扫描配置文件 +// @Accept json +// @Param request body dto.UpdateByNameAndFile true "request" +// @Success 200 +// @Security ApiKeyAuth +// @Router /toolbox/clam/file/update [post] +func (b *BaseApi) UpdateFile(c *gin.Context) { + var req dto.UpdateByNameAndFile + if err := helper.CheckBindAndValidate(&req, c); err != nil { + return + } + if err := clamService.UpdateFile(req); err != nil { + helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) + return + } + helper.SuccessWithOutData(c) +} + +// @Tags Clam +// @Summary Delete clam +// @Description 删除扫描规则 +// @Accept json +// @Param request body dto.ClamDelete true "request" +// @Success 200 +// @Security ApiKeyAuth +// @Router /toolbox/clam/del [post] +// @x-panel-log {"bodyKeys":["ids"],"paramKeys":[],"BeforeFunctions":[{"input_column":"id","input_value":"ids","isList":true,"db":"clams","output_column":"name","output_value":"names"}],"formatZH":"删除扫描规则 [names]","formatEN":"delete clam [names]"} +func (b *BaseApi) DeleteClam(c *gin.Context) { + var req dto.ClamDelete + if err := helper.CheckBindAndValidate(&req, c); err != nil { + return + } + + if err := clamService.Delete(req); err != nil { + helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) + return + } + helper.SuccessWithData(c, nil) +} + +// @Tags Clam +// @Summary Handle clam scan +// @Description 执行病毒扫描 +// @Accept json +// @Param request body dto.OperateByID true "request" +// @Success 200 +// @Security ApiKeyAuth +// @Router /toolbox/clam/handle [post] +// @x-panel-log {"bodyKeys":["id"],"paramKeys":[],"BeforeFunctions":[{"input_column":"id","input_value":"id","isList":true,"db":"clams","output_column":"name","output_value":"name"}],"formatZH":"执行病毒扫描 [name]","formatEN":"handle clam scan [name]"} +func (b *BaseApi) HandleClamScan(c *gin.Context) { + var req dto.OperateByID + if err := helper.CheckBindAndValidate(&req, c); err != nil { + return + } + + if err := clamService.HandleOnce(req); err != nil { + helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) + return + } + helper.SuccessWithData(c, nil) +} diff --git a/agent/app/api/v1/command.go b/agent/app/api/v1/command.go new file mode 100644 index 000000000..bebcb7e10 --- /dev/null +++ b/agent/app/api/v1/command.go @@ -0,0 +1,223 @@ +package v1 + +import ( + "github.com/1Panel-dev/1Panel/agent/app/api/v1/helper" + "github.com/1Panel-dev/1Panel/agent/app/dto" + "github.com/1Panel-dev/1Panel/agent/constant" + "github.com/gin-gonic/gin" +) + +// @Tags Command +// @Summary Create command +// @Description 创建快速命令 +// @Accept json +// @Param request body dto.CommandOperate true "request" +// @Success 200 +// @Security ApiKeyAuth +// @Router /hosts/command [post] +// @x-panel-log {"bodyKeys":["name","command"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"创建快捷命令 [name][command]","formatEN":"create quick command [name][command]"} +func (b *BaseApi) CreateCommand(c *gin.Context) { + var req dto.CommandOperate + if err := helper.CheckBindAndValidate(&req, c); err != nil { + return + } + + if err := commandService.Create(req); err != nil { + helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) + return + } + helper.SuccessWithData(c, nil) +} + +// @Tags Redis Command +// @Summary Save redis command +// @Description 保存 Redis 快速命令 +// @Accept json +// @Param request body dto.RedisCommand true "request" +// @Success 200 +// @Security ApiKeyAuth +// @Router /hosts/command/redis [post] +// @x-panel-log {"bodyKeys":["name","command"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"保存 redis 快捷命令 [name][command]","formatEN":"save quick command for redis [name][command]"} +func (b *BaseApi) SaveRedisCommand(c *gin.Context) { + var req dto.RedisCommand + if err := helper.CheckBindAndValidate(&req, c); err != nil { + return + } + + if err := commandService.SaveRedisCommand(req); err != nil { + helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) + return + } + helper.SuccessWithData(c, nil) +} + +// @Tags Command +// @Summary Page commands +// @Description 获取快速命令列表分页 +// @Accept json +// @Param request body dto.SearchWithPage true "request" +// @Success 200 {object} dto.PageResult +// @Security ApiKeyAuth +// @Router /hosts/command/search [post] +func (b *BaseApi) SearchCommand(c *gin.Context) { + var req dto.SearchCommandWithPage + if err := helper.CheckBindAndValidate(&req, c); err != nil { + return + } + + total, list, err := commandService.SearchWithPage(req) + if err != nil { + helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) + return + } + + helper.SuccessWithData(c, dto.PageResult{ + Items: list, + Total: total, + }) +} + +// @Tags Redis Command +// @Summary Page redis commands +// @Description 获取 redis 快速命令列表分页 +// @Accept json +// @Param request body dto.SearchWithPage true "request" +// @Success 200 {object} dto.PageResult +// @Security ApiKeyAuth +// @Router /hosts/command/redis/search [post] +func (b *BaseApi) SearchRedisCommand(c *gin.Context) { + var req dto.SearchWithPage + if err := helper.CheckBindAndValidate(&req, c); err != nil { + return + } + + total, list, err := commandService.SearchRedisCommandWithPage(req) + if err != nil { + helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) + return + } + + helper.SuccessWithData(c, dto.PageResult{ + Items: list, + Total: total, + }) +} + +// @Tags Command +// @Summary Tree commands +// @Description 获取快速命令树 +// @Accept json +// @Success 200 {Array} dto.CommandTree +// @Security ApiKeyAuth +// @Router /hosts/command/tree [get] +func (b *BaseApi) SearchCommandTree(c *gin.Context) { + list, err := commandService.SearchForTree() + if err != nil { + helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) + return + } + + helper.SuccessWithData(c, list) +} + +// @Tags Redis Command +// @Summary List redis commands +// @Description 获取 redis 快速命令列表 +// @Success 200 {Array} dto.RedisCommand +// @Security ApiKeyAuth +// @Router /hosts/command/redis [get] +func (b *BaseApi) ListRedisCommand(c *gin.Context) { + list, err := commandService.ListRedisCommand() + if err != nil { + helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) + return + } + + helper.SuccessWithData(c, list) +} + +// @Tags Command +// @Summary List commands +// @Description 获取快速命令列表 +// @Success 200 {object} dto.CommandInfo +// @Security ApiKeyAuth +// @Router /hosts/command [get] +func (b *BaseApi) ListCommand(c *gin.Context) { + list, err := commandService.List() + if err != nil { + helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) + return + } + + helper.SuccessWithData(c, list) +} + +// @Tags Command +// @Summary Delete command +// @Description 删除快速命令 +// @Accept json +// @Param request body dto.BatchDeleteReq true "request" +// @Success 200 +// @Security ApiKeyAuth +// @Router /hosts/command/del [post] +// @x-panel-log {"bodyKeys":["ids"],"paramKeys":[],"BeforeFunctions":[{"input_column":"id","input_value":"ids","isList":true,"db":"commands","output_column":"name","output_value":"names"}],"formatZH":"删除快捷命令 [names]","formatEN":"delete quick command [names]"} +func (b *BaseApi) DeleteCommand(c *gin.Context) { + var req dto.BatchDeleteReq + if err := helper.CheckBindAndValidate(&req, c); err != nil { + return + } + + if err := commandService.Delete(req.Ids); err != nil { + helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) + return + } + helper.SuccessWithData(c, nil) +} + +// @Tags Redis Command +// @Summary Delete redis command +// @Description 删除 redis 快速命令 +// @Accept json +// @Param request body dto.BatchDeleteReq true "request" +// @Success 200 +// @Security ApiKeyAuth +// @Router /hosts/command/redis/del [post] +// @x-panel-log {"bodyKeys":["ids"],"paramKeys":[],"BeforeFunctions":[{"input_column":"id","input_value":"ids","isList":true,"db":"redis_commands","output_column":"name","output_value":"names"}],"formatZH":"删除 redis 快捷命令 [names]","formatEN":"delete quick command of redis [names]"} +func (b *BaseApi) DeleteRedisCommand(c *gin.Context) { + var req dto.BatchDeleteReq + if err := helper.CheckBindAndValidate(&req, c); err != nil { + return + } + + if err := commandService.DeleteRedisCommand(req.Ids); err != nil { + helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) + return + } + helper.SuccessWithData(c, nil) +} + +// @Tags Command +// @Summary Update command +// @Description 更新快速命令 +// @Accept json +// @Param request body dto.CommandOperate true "request" +// @Success 200 +// @Security ApiKeyAuth +// @Router /hosts/command/update [post] +// @x-panel-log {"bodyKeys":["name"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"更新快捷命令 [name]","formatEN":"update quick command [name]"} +func (b *BaseApi) UpdateCommand(c *gin.Context) { + var req dto.CommandOperate + if err := helper.CheckBindAndValidate(&req, c); err != nil { + return + } + + upMap := make(map[string]interface{}) + upMap["name"] = req.Name + upMap["group_id"] = req.GroupID + upMap["command"] = req.Command + if err := commandService.Update(req.ID, upMap); err != nil { + helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) + return + } + helper.SuccessWithData(c, nil) +} diff --git a/agent/app/api/v1/compose_template.go b/agent/app/api/v1/compose_template.go new file mode 100644 index 000000000..c2058def8 --- /dev/null +++ b/agent/app/api/v1/compose_template.go @@ -0,0 +1,121 @@ +package v1 + +import ( + "github.com/1Panel-dev/1Panel/agent/app/api/v1/helper" + "github.com/1Panel-dev/1Panel/agent/app/dto" + "github.com/1Panel-dev/1Panel/agent/constant" + "github.com/gin-gonic/gin" +) + +// @Tags Container Compose-template +// @Summary Create compose template +// @Description 创建容器编排模版 +// @Accept json +// @Param request body dto.ComposeTemplateCreate true "request" +// @Success 200 +// @Security ApiKeyAuth +// @Router /containers/template [post] +// @x-panel-log {"bodyKeys":["name"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"创建 compose 模版 [name]","formatEN":"create compose template [name]"} +func (b *BaseApi) CreateComposeTemplate(c *gin.Context) { + var req dto.ComposeTemplateCreate + if err := helper.CheckBindAndValidate(&req, c); err != nil { + return + } + + if err := composeTemplateService.Create(req); err != nil { + helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) + return + } + helper.SuccessWithData(c, nil) +} + +// @Tags Container Compose-template +// @Summary Page compose templates +// @Description 获取容器编排模版列表分页 +// @Accept json +// @Param request body dto.SearchWithPage true "request" +// @Produce json +// @Success 200 {object} dto.PageResult +// @Security ApiKeyAuth +// @Router /containers/template/search [post] +func (b *BaseApi) SearchComposeTemplate(c *gin.Context) { + var req dto.SearchWithPage + if err := helper.CheckBindAndValidate(&req, c); err != nil { + return + } + + total, list, err := composeTemplateService.SearchWithPage(req) + if err != nil { + helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) + return + } + + helper.SuccessWithData(c, dto.PageResult{ + Items: list, + Total: total, + }) +} + +// @Tags Container Compose-template +// @Summary List compose templates +// @Description 获取容器编排模版列表 +// @Produce json +// @Success 200 {array} dto.ComposeTemplateInfo +// @Security ApiKeyAuth +// @Router /containers/template [get] +func (b *BaseApi) ListComposeTemplate(c *gin.Context) { + list, err := composeTemplateService.List() + if err != nil { + helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) + return + } + + helper.SuccessWithData(c, list) +} + +// @Tags Container Compose-template +// @Summary Delete compose template +// @Description 删除容器编排模版 +// @Accept json +// @Param request body dto.BatchDelete true "request" +// @Success 200 +// @Security ApiKeyAuth +// @Router /containers/template/del [post] +// @x-panel-log {"bodyKeys":["ids"],"paramKeys":[],"BeforeFunctions":[{"input_column":"id","input_value":"ids","isList":true,"db":"compose_templates","output_column":"name","output_value":"names"}],"formatZH":"删除 compose 模版 [names]","formatEN":"delete compose template [names]"} +func (b *BaseApi) DeleteComposeTemplate(c *gin.Context) { + var req dto.BatchDeleteReq + if err := helper.CheckBindAndValidate(&req, c); err != nil { + return + } + + if err := composeTemplateService.Delete(req.Ids); err != nil { + helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) + return + } + helper.SuccessWithData(c, nil) +} + +// @Tags Container Compose-template +// @Summary Update compose template +// @Description 更新容器编排模版 +// @Accept json +// @Param request body dto.ComposeTemplateUpdate true "request" +// @Success 200 +// @Security ApiKeyAuth +// @Router /containers/template/update [post] +// @x-panel-log {"bodyKeys":["id"],"paramKeys":[],"BeforeFunctions":[{"input_column":"id","input_value":"id","isList":false,"db":"compose_templates","output_column":"name","output_value":"name"}],"formatZH":"更新 compose 模版 [name]","formatEN":"update compose template information [name]"} +func (b *BaseApi) UpdateComposeTemplate(c *gin.Context) { + var req dto.ComposeTemplateUpdate + if err := helper.CheckBindAndValidate(&req, c); err != nil { + return + } + + upMap := make(map[string]interface{}) + upMap["content"] = req.Content + upMap["description"] = req.Description + if err := composeTemplateService.Update(req.ID, upMap); err != nil { + helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) + return + } + helper.SuccessWithData(c, nil) +} diff --git a/agent/app/api/v1/container.go b/agent/app/api/v1/container.go new file mode 100644 index 000000000..c4f095b64 --- /dev/null +++ b/agent/app/api/v1/container.go @@ -0,0 +1,700 @@ +package v1 + +import ( + "strconv" + + "github.com/1Panel-dev/1Panel/agent/app/api/v1/helper" + "github.com/1Panel-dev/1Panel/agent/app/dto" + "github.com/1Panel-dev/1Panel/agent/constant" + "github.com/1Panel-dev/1Panel/agent/global" + "github.com/gin-gonic/gin" + "github.com/pkg/errors" +) + +// @Tags Container +// @Summary Page containers +// @Description 获取容器列表分页 +// @Accept json +// @Param request body dto.PageContainer true "request" +// @Produce json +// @Success 200 {object} dto.PageResult +// @Security ApiKeyAuth +// @Router /containers/search [post] +func (b *BaseApi) SearchContainer(c *gin.Context) { + var req dto.PageContainer + if err := helper.CheckBindAndValidate(&req, c); err != nil { + return + } + + total, list, err := containerService.Page(req) + if err != nil { + helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) + return + } + helper.SuccessWithData(c, dto.PageResult{ + Items: list, + Total: total, + }) +} + +// @Tags Container +// @Summary List containers +// @Description 获取容器名称 +// @Accept json +// @Produce json +// @Success 200 +// @Security ApiKeyAuth +// @Router /containers/list [post] +func (b *BaseApi) ListContainer(c *gin.Context) { + list, err := containerService.List() + if err != nil { + helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) + return + } + helper.SuccessWithData(c, list) +} + +// @Tags Container Compose +// @Summary Page composes +// @Description 获取编排列表分页 +// @Accept json +// @Param request body dto.SearchWithPage true "request" +// @Success 200 {object} dto.PageResult +// @Security ApiKeyAuth +// @Router /containers/compose/search [post] +func (b *BaseApi) SearchCompose(c *gin.Context) { + var req dto.SearchWithPage + if err := helper.CheckBindAndValidate(&req, c); err != nil { + return + } + + total, list, err := containerService.PageCompose(req) + if err != nil { + helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) + return + } + helper.SuccessWithData(c, dto.PageResult{ + Items: list, + Total: total, + }) +} + +// @Tags Container Compose +// @Summary Test compose +// @Description 测试 compose 是否可用 +// @Accept json +// @Param request body dto.ComposeCreate true "request" +// @Success 200 +// @Security ApiKeyAuth +// @Router /containers/compose/test [post] +// @x-panel-log {"bodyKeys":["name"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"检测 compose [name] 格式","formatEN":"check compose [name]"} +func (b *BaseApi) TestCompose(c *gin.Context) { + var req dto.ComposeCreate + if err := helper.CheckBindAndValidate(&req, c); err != nil { + return + } + + isOK, err := containerService.TestCompose(req) + if err != nil { + helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) + return + } + helper.SuccessWithData(c, isOK) +} + +// @Tags Container Compose +// @Summary Create compose +// @Description 创建容器编排 +// @Accept json +// @Param request body dto.ComposeCreate true "request" +// @Success 200 +// @Security ApiKeyAuth +// @Router /containers/compose [post] +// @x-panel-log {"bodyKeys":["name"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"创建 compose [name]","formatEN":"create compose [name]"} +func (b *BaseApi) CreateCompose(c *gin.Context) { + var req dto.ComposeCreate + if err := helper.CheckBindAndValidate(&req, c); err != nil { + return + } + + log, err := containerService.CreateCompose(req) + if err != nil { + helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) + return + } + helper.SuccessWithData(c, log) +} + +// @Tags Container Compose +// @Summary Operate compose +// @Description 容器编排操作 +// @Accept json +// @Param request body dto.ComposeOperation true "request" +// @Success 200 +// @Security ApiKeyAuth +// @Router /containers/compose/operate [post] +// @x-panel-log {"bodyKeys":["name","operation"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"compose [operation] [name]","formatEN":"compose [operation] [name]"} +func (b *BaseApi) OperatorCompose(c *gin.Context) { + var req dto.ComposeOperation + if err := helper.CheckBindAndValidate(&req, c); err != nil { + return + } + + if err := containerService.ComposeOperation(req); err != nil { + helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) + return + } + helper.SuccessWithData(c, nil) +} + +// @Tags Container +// @Summary Update container +// @Description 更新容器 +// @Accept json +// @Param request body dto.ContainerOperate true "request" +// @Success 200 +// @Security ApiKeyAuth +// @Router /containers/update [post] +// @x-panel-log {"bodyKeys":["name","image"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"更新容器 [name][image]","formatEN":"update container [name][image]"} +func (b *BaseApi) ContainerUpdate(c *gin.Context) { + var req dto.ContainerOperate + if err := helper.CheckBindAndValidate(&req, c); err != nil { + return + } + + if err := containerService.ContainerUpdate(req); err != nil { + helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) + return + } + helper.SuccessWithData(c, nil) +} + +// @Tags Container +// @Summary Load container info +// @Description 获取容器表单信息 +// @Accept json +// @Param request body dto.OperationWithName true "request" +// @Success 200 {object} dto.ContainerOperate +// @Security ApiKeyAuth +// @Router /containers/info [post] +func (b *BaseApi) ContainerInfo(c *gin.Context) { + var req dto.OperationWithName + if err := helper.CheckBindAndValidate(&req, c); err != nil { + return + } + + data, err := containerService.ContainerInfo(req) + if err != nil { + helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) + return + } + helper.SuccessWithData(c, data) +} + +// @Summary Load container limits +// @Description 获取容器限制 +// @Success 200 {object} dto.ResourceLimit +// @Security ApiKeyAuth +// @Router /containers/limit [get] +func (b *BaseApi) LoadResourceLimit(c *gin.Context) { + data, err := containerService.LoadResourceLimit() + if err != nil { + helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) + return + } + helper.SuccessWithData(c, data) +} + +// @Summary Load container stats +// @Description 获取容器列表资源占用 +// @Success 200 {array} dto.ContainerListStats +// @Security ApiKeyAuth +// @Router /containers/list/stats [get] +func (b *BaseApi) ContainerListStats(c *gin.Context) { + data, err := containerService.ContainerListStats() + if err != nil { + helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) + return + } + helper.SuccessWithData(c, data) +} + +// @Tags Container +// @Summary Create container +// @Description 创建容器 +// @Accept json +// @Param request body dto.ContainerOperate true "request" +// @Success 200 +// @Security ApiKeyAuth +// @Router /containers [post] +// @x-panel-log {"bodyKeys":["name","image"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"创建容器 [name][image]","formatEN":"create container [name][image]"} +func (b *BaseApi) ContainerCreate(c *gin.Context) { + var req dto.ContainerOperate + if err := helper.CheckBindAndValidate(&req, c); err != nil { + return + } + + if err := containerService.ContainerCreate(req); err != nil { + helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) + return + } + helper.SuccessWithData(c, nil) +} + +// @Tags Container +// @Summary Upgrade container +// @Description 更新容器镜像 +// @Accept json +// @Param request body dto.ContainerUpgrade true "request" +// @Success 200 +// @Security ApiKeyAuth +// @Router /containers/upgrade [post] +// @x-panel-log {"bodyKeys":["name","image"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"更新容器镜像 [name][image]","formatEN":"upgrade container image [name][image]"} +func (b *BaseApi) ContainerUpgrade(c *gin.Context) { + var req dto.ContainerUpgrade + if err := helper.CheckBindAndValidate(&req, c); err != nil { + return + } + + if err := containerService.ContainerUpgrade(req); err != nil { + helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) + return + } + helper.SuccessWithData(c, nil) +} + +// @Tags Container +// @Summary Clean container +// @Description 容器清理 +// @Accept json +// @Param request body dto.ContainerPrune true "request" +// @Success 200 {object} dto.ContainerPruneReport +// @Security ApiKeyAuth +// @Router /containers/prune [post] +// @x-panel-log {"bodyKeys":["pruneType"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"清理容器 [pruneType]","formatEN":"clean container [pruneType]"} +func (b *BaseApi) ContainerPrune(c *gin.Context) { + var req dto.ContainerPrune + if err := helper.CheckBindAndValidate(&req, c); err != nil { + return + } + + report, err := containerService.Prune(req) + if err != nil { + helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) + return + } + helper.SuccessWithData(c, report) +} + +// @Tags Container +// @Summary Clean container log +// @Description 清理容器日志 +// @Accept json +// @Param request body dto.OperationWithName true "request" +// @Success 200 +// @Security ApiKeyAuth +// @Router /containers/clean/log [post] +// @x-panel-log {"bodyKeys":["name"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"清理容器 [name] 日志","formatEN":"clean container [name] logs"} +func (b *BaseApi) CleanContainerLog(c *gin.Context) { + var req dto.OperationWithName + if err := helper.CheckBindAndValidate(&req, c); err != nil { + return + } + + if err := containerService.ContainerLogClean(req); err != nil { + helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) + return + } + helper.SuccessWithData(c, nil) +} + +// @Tags Container +// @Summary Load container log +// @Description 获取容器操作日志 +// @Accept json +// @Param request body dto.OperationWithNameAndType true "request" +// @Success 200 +// @Security ApiKeyAuth +// @Router /containers/load/log [post] +func (b *BaseApi) LoadContainerLog(c *gin.Context) { + var req dto.OperationWithNameAndType + if err := helper.CheckBindAndValidate(&req, c); err != nil { + return + } + + content := containerService.LoadContainerLogs(req) + helper.SuccessWithData(c, content) +} + +// @Tags Container +// @Summary Rename Container +// @Description 容器重命名 +// @Accept json +// @Param request body dto.ContainerRename true "request" +// @Success 200 +// @Security ApiKeyAuth +// @Router /containers/rename [post] +// @x-panel-log {"bodyKeys":["name","newName"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"容器重命名 [name] => [newName]","formatEN":"rename container [name] => [newName]"} +func (b *BaseApi) ContainerRename(c *gin.Context) { + var req dto.ContainerRename + if err := helper.CheckBindAndValidate(&req, c); err != nil { + return + } + + if err := containerService.ContainerRename(req); err != nil { + helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) + return + } + helper.SuccessWithData(c, nil) +} + +// @Tags Container +// @Summary Commit Container +// @Description 容器提交生成新镜像 +// @Accept json +// @Param request body dto.ContainerCommit true "request" +// @Success 200 +// @Router /containers/commit [post] +func (b *BaseApi) ContainerCommit(c *gin.Context) { + var req dto.ContainerCommit + if err := helper.CheckBindAndValidate(&req, c); err != nil { + return + } + + if err := containerService.ContainerCommit(req); err != nil { + helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) + return + } + helper.SuccessWithData(c, nil) +} + +// @Tags Container +// @Summary Operate Container +// @Description 容器操作 +// @Accept json +// @Param request body dto.ContainerOperation true "request" +// @Success 200 +// @Security ApiKeyAuth +// @Router /containers/operate [post] +// @x-panel-log {"bodyKeys":["names","operation"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"容器 [names] 执行 [operation]","formatEN":"container [operation] [names]"} +func (b *BaseApi) ContainerOperation(c *gin.Context) { + var req dto.ContainerOperation + if err := helper.CheckBindAndValidate(&req, c); err != nil { + return + } + + if err := containerService.ContainerOperation(req); err != nil { + helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) + return + } + helper.SuccessWithData(c, nil) +} + +// @Tags Container +// @Summary Container stats +// @Description 容器监控信息 +// @Param id path integer true "容器id" +// @Success 200 {object} dto.ContainerStats +// @Security ApiKeyAuth +// @Router /containers/stats/:id [get] +func (b *BaseApi) ContainerStats(c *gin.Context) { + containerID, ok := c.Params.Get("id") + if !ok { + helper.ErrorWithDetail(c, constant.CodeErrBadRequest, constant.ErrTypeInvalidParams, errors.New("error container id in path")) + return + } + + result, err := containerService.ContainerStats(containerID) + if err != nil { + helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) + return + } + helper.SuccessWithData(c, result) +} + +// @Tags Container +// @Summary Container inspect +// @Description 容器详情 +// @Accept json +// @Param request body dto.InspectReq true "request" +// @Success 200 {string} result +// @Security ApiKeyAuth +// @Router /containers/inspect [post] +func (b *BaseApi) Inspect(c *gin.Context) { + var req dto.InspectReq + if err := helper.CheckBindAndValidate(&req, c); err != nil { + return + } + + result, err := containerService.Inspect(req) + if err != nil { + helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) + return + } + helper.SuccessWithData(c, result) +} + +// @Tags Container +// @Summary Container logs +// @Description 容器日志 +// @Param container query string false "容器名称" +// @Param since query string false "时间筛选" +// @Param follow query string false "是否追踪" +// @Param tail query string false "显示行号" +// @Security ApiKeyAuth +// @Router /containers/search/log [post] +func (b *BaseApi) ContainerLogs(c *gin.Context) { + wsConn, err := upGrader.Upgrade(c.Writer, c.Request, nil) + if err != nil { + global.LOG.Errorf("gin context http handler failed, err: %v", err) + return + } + defer wsConn.Close() + + container := c.Query("container") + since := c.Query("since") + follow := c.Query("follow") == "true" + tail := c.Query("tail") + + if err := containerService.ContainerLogs(wsConn, "container", container, since, tail, follow); err != nil { + _ = wsConn.WriteMessage(1, []byte(err.Error())) + return + } +} + +// @Description 下载容器日志 +// @Router /containers/download/log [post] +func (b *BaseApi) DownloadContainerLogs(c *gin.Context) { + var req dto.ContainerLog + if err := helper.CheckBindAndValidate(&req, c); err != nil { + return + } + err := containerService.DownloadContainerLogs(req.ContainerType, req.Container, req.Since, strconv.Itoa(int(req.Tail)), c) + if err != nil { + helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) + } +} + +// @Tags Container Network +// @Summary Page networks +// @Description 获取容器网络列表分页 +// @Accept json +// @Param request body dto.SearchWithPage true "request" +// @Produce json +// @Success 200 {object} dto.PageResult +// @Security ApiKeyAuth +// @Router /containers/network/search [post] +func (b *BaseApi) SearchNetwork(c *gin.Context) { + var req dto.SearchWithPage + if err := helper.CheckBindAndValidate(&req, c); err != nil { + return + } + + total, list, err := containerService.PageNetwork(req) + if err != nil { + helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) + return + } + helper.SuccessWithData(c, dto.PageResult{ + Items: list, + Total: total, + }) +} + +// @Tags Container Network +// @Summary List networks +// @Description 获取容器网络列表 +// @Accept json +// @Produce json +// @Success 200 {array} dto.Options +// @Security ApiKeyAuth +// @Router /containers/network [get] +func (b *BaseApi) ListNetwork(c *gin.Context) { + list, err := containerService.ListNetwork() + if err != nil { + helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) + return + } + helper.SuccessWithData(c, list) +} + +// @Tags Container Network +// @Summary Delete network +// @Description 删除容器网络 +// @Accept json +// @Param request body dto.BatchDelete true "request" +// @Success 200 +// @Security ApiKeyAuth +// @Router /containers/network/del [post] +// @x-panel-log {"bodyKeys":["names"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"删除容器网络 [names]","formatEN":"delete container network [names]"} +func (b *BaseApi) DeleteNetwork(c *gin.Context) { + var req dto.BatchDelete + if err := helper.CheckBindAndValidate(&req, c); err != nil { + return + } + + if err := containerService.DeleteNetwork(req); err != nil { + helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) + return + } + helper.SuccessWithData(c, nil) +} + +// @Tags Container Network +// @Summary Create network +// @Description 创建容器网络 +// @Accept json +// @Param request body dto.NetworkCreate true "request" +// @Success 200 +// @Security ApiKeyAuth +// @Router /containers/network [post] +// @x-panel-log {"bodyKeys":["name"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"创建容器网络 name","formatEN":"create container network [name]"} +func (b *BaseApi) CreateNetwork(c *gin.Context) { + var req dto.NetworkCreate + if err := helper.CheckBindAndValidate(&req, c); err != nil { + return + } + + if err := containerService.CreateNetwork(req); err != nil { + helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) + return + } + helper.SuccessWithData(c, nil) +} + +// @Tags Container Volume +// @Summary Page volumes +// @Description 获取容器存储卷分页 +// @Accept json +// @Param request body dto.SearchWithPage true "request" +// @Produce json +// @Success 200 {object} dto.PageResult +// @Security ApiKeyAuth +// @Router /containers/volume/search [post] +func (b *BaseApi) SearchVolume(c *gin.Context) { + var req dto.SearchWithPage + if err := helper.CheckBindAndValidate(&req, c); err != nil { + return + } + + total, list, err := containerService.PageVolume(req) + if err != nil { + helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) + return + } + helper.SuccessWithData(c, dto.PageResult{ + Items: list, + Total: total, + }) +} + +// @Tags Container Volume +// @Summary List volumes +// @Description 获取容器存储卷列表 +// @Accept json +// @Produce json +// @Success 200 {array} dto.Options +// @Security ApiKeyAuth +// @Router /containers/volume [get] +func (b *BaseApi) ListVolume(c *gin.Context) { + list, err := containerService.ListVolume() + if err != nil { + helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) + return + } + helper.SuccessWithData(c, list) +} + +// @Tags Container Volume +// @Summary Delete volume +// @Description 删除容器存储卷 +// @Accept json +// @Param request body dto.BatchDelete true "request" +// @Success 200 +// @Security ApiKeyAuth +// @Router /containers/volume/del [post] +// @x-panel-log {"bodyKeys":["names"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"删除容器存储卷 [names]","formatEN":"delete container volume [names]"} +func (b *BaseApi) DeleteVolume(c *gin.Context) { + var req dto.BatchDelete + if err := helper.CheckBindAndValidate(&req, c); err != nil { + return + } + + if err := containerService.DeleteVolume(req); err != nil { + helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) + return + } + helper.SuccessWithData(c, nil) +} + +// @Tags Container Volume +// @Summary Create volume +// @Description 创建容器存储卷 +// @Accept json +// @Param request body dto.VolumeCreate true "request" +// @Success 200 +// @Security ApiKeyAuth +// @Router /containers/volume [post] +// @x-panel-log {"bodyKeys":["name"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"创建容器存储卷 [name]","formatEN":"create container volume [name]"} +func (b *BaseApi) CreateVolume(c *gin.Context) { + var req dto.VolumeCreate + if err := helper.CheckBindAndValidate(&req, c); err != nil { + return + } + + if err := containerService.CreateVolume(req); err != nil { + helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) + return + } + helper.SuccessWithData(c, nil) +} + +// @Tags Container Compose +// @Summary Update compose +// @Description 更新容器编排 +// @Accept json +// @Param request body dto.ComposeUpdate true "request" +// @Success 200 +// @Security ApiKeyAuth +// @Router /containers/compose/update [post] +// @x-panel-log {"bodyKeys":["name"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"更新 compose [name]","formatEN":"update compose information [name]"} +func (b *BaseApi) ComposeUpdate(c *gin.Context) { + var req dto.ComposeUpdate + if err := helper.CheckBindAndValidate(&req, c); err != nil { + return + } + + if err := containerService.ComposeUpdate(req); err != nil { + helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) + return + } + helper.SuccessWithData(c, nil) +} + +// @Tags Container Compose +// @Summary Container Compose logs +// @Description docker-compose 日志 +// @Param compose query string false "compose 文件地址" +// @Param since query string false "时间筛选" +// @Param follow query string false "是否追踪" +// @Param tail query string false "显示行号" +// @Security ApiKeyAuth +// @Router /containers/compose/search/log [get] +func (b *BaseApi) ComposeLogs(c *gin.Context) { + wsConn, err := upGrader.Upgrade(c.Writer, c.Request, nil) + if err != nil { + global.LOG.Errorf("gin context http handler failed, err: %v", err) + return + } + defer wsConn.Close() + + compose := c.Query("compose") + since := c.Query("since") + follow := c.Query("follow") == "true" + tail := c.Query("tail") + + if err := containerService.ContainerLogs(wsConn, "compose", compose, since, tail, follow); err != nil { + _ = wsConn.WriteMessage(1, []byte(err.Error())) + return + } +} diff --git a/agent/app/api/v1/cronjob.go b/agent/app/api/v1/cronjob.go new file mode 100644 index 000000000..869d18bcf --- /dev/null +++ b/agent/app/api/v1/cronjob.go @@ -0,0 +1,241 @@ +package v1 + +import ( + "time" + + "github.com/1Panel-dev/1Panel/agent/app/api/v1/helper" + "github.com/1Panel-dev/1Panel/agent/app/dto" + "github.com/1Panel-dev/1Panel/agent/constant" + "github.com/1Panel-dev/1Panel/agent/utils/common" + "github.com/gin-gonic/gin" +) + +// @Tags Cronjob +// @Summary Create cronjob +// @Description 创建计划任务 +// @Accept json +// @Param request body dto.CronjobCreate true "request" +// @Success 200 +// @Security ApiKeyAuth +// @Router /cronjobs [post] +// @x-panel-log {"bodyKeys":["type","name"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"创建计划任务 [type][name]","formatEN":"create cronjob [type][name]"} +func (b *BaseApi) CreateCronjob(c *gin.Context) { + var req dto.CronjobCreate + if err := helper.CheckBindAndValidate(&req, c); err != nil { + return + } + + if err := cronjobService.Create(req); err != nil { + helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) + return + } + helper.SuccessWithData(c, nil) +} + +// @Tags Cronjob +// @Summary Page cronjobs +// @Description 获取计划任务分页 +// @Accept json +// @Param request body dto.PageCronjob true "request" +// @Success 200 {object} dto.PageResult +// @Security ApiKeyAuth +// @Router /cronjobs/search [post] +func (b *BaseApi) SearchCronjob(c *gin.Context) { + var req dto.PageCronjob + if err := helper.CheckBindAndValidate(&req, c); err != nil { + return + } + + total, list, err := cronjobService.SearchWithPage(req) + if err != nil { + helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) + return + } + + helper.SuccessWithData(c, dto.PageResult{ + Items: list, + Total: total, + }) +} + +// @Tags Cronjob +// @Summary Page job records +// @Description 获取计划任务记录 +// @Accept json +// @Param request body dto.SearchRecord true "request" +// @Success 200 {object} dto.PageResult +// @Security ApiKeyAuth +// @Router /cronjobs/search/records [post] +func (b *BaseApi) SearchJobRecords(c *gin.Context) { + var req dto.SearchRecord + if err := helper.CheckBindAndValidate(&req, c); err != nil { + return + } + + loc, _ := time.LoadLocation(common.LoadTimeZone()) + req.StartTime = req.StartTime.In(loc) + req.EndTime = req.EndTime.In(loc) + + total, list, err := cronjobService.SearchRecords(req) + if err != nil { + helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) + return + } + + helper.SuccessWithData(c, dto.PageResult{ + Items: list, + Total: total, + }) +} + +// @Tags Cronjob +// @Summary Load Cronjob record log +// @Description 获取计划任务记录日志 +// @Accept json +// @Param request body dto.OperateByID true "request" +// @Success 200 +// @Security ApiKeyAuth +// @Router /cronjobs/records/log [post] +func (b *BaseApi) LoadRecordLog(c *gin.Context) { + var req dto.OperateByID + if err := helper.CheckBindAndValidate(&req, c); err != nil { + return + } + content := cronjobService.LoadRecordLog(req) + helper.SuccessWithData(c, content) +} + +// @Tags Cronjob +// @Summary Clean job records +// @Description 清空计划任务记录 +// @Accept json +// @Param request body dto.CronjobClean true "request" +// @Success 200 +// @Security ApiKeyAuth +// @Router /cronjobs/records/clean [post] +// @x-panel-log {"bodyKeys":["id"],"paramKeys":[],"BeforeFunctions":[{"input_column":"id","input_value":"id","isList":false,"db":"cronjobs","output_column":"name","output_value":"name"}],"formatZH":"清空计划任务记录 [name]","formatEN":"clean cronjob [name] records"} +func (b *BaseApi) CleanRecord(c *gin.Context) { + var req dto.CronjobClean + if err := helper.CheckBindAndValidate(&req, c); err != nil { + return + } + + if err := cronjobService.CleanRecord(req); err != nil { + helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) + return + } + + helper.SuccessWithData(c, nil) +} + +// @Tags Cronjob +// @Summary Delete cronjob +// @Description 删除计划任务 +// @Accept json +// @Param request body dto.CronjobBatchDelete true "request" +// @Success 200 +// @Security ApiKeyAuth +// @Router /cronjobs/del [post] +// @x-panel-log {"bodyKeys":["ids"],"paramKeys":[],"BeforeFunctions":[{"input_column":"id","input_value":"ids","isList":true,"db":"cronjobs","output_column":"name","output_value":"names"}],"formatZH":"删除计划任务 [names]","formatEN":"delete cronjob [names]"} +func (b *BaseApi) DeleteCronjob(c *gin.Context) { + var req dto.CronjobBatchDelete + if err := helper.CheckBindAndValidate(&req, c); err != nil { + return + } + + if err := cronjobService.Delete(req); err != nil { + helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) + return + } + helper.SuccessWithData(c, nil) +} + +// @Tags Cronjob +// @Summary Update cronjob +// @Description 更新计划任务 +// @Accept json +// @Param request body dto.CronjobUpdate true "request" +// @Success 200 +// @Security ApiKeyAuth +// @Router /cronjobs/update [post] +// @x-panel-log {"bodyKeys":["id"],"paramKeys":[],"BeforeFunctions":[{"input_column":"id","input_value":"id","isList":false,"db":"cronjobs","output_column":"name","output_value":"name"}],"formatZH":"更新计划任务 [name]","formatEN":"update cronjob [name]"} +func (b *BaseApi) UpdateCronjob(c *gin.Context) { + var req dto.CronjobUpdate + if err := helper.CheckBindAndValidate(&req, c); err != nil { + return + } + + if err := cronjobService.Update(req.ID, req); err != nil { + helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) + return + } + helper.SuccessWithData(c, nil) +} + +// @Tags Cronjob +// @Summary Update cronjob status +// @Description 更新计划任务状态 +// @Accept json +// @Param request body dto.CronjobUpdateStatus true "request" +// @Success 200 +// @Security ApiKeyAuth +// @Router /cronjobs/status [post] +// @x-panel-log {"bodyKeys":["id","status"],"paramKeys":[],"BeforeFunctions":[{"input_column":"id","input_value":"id","isList":false,"db":"cronjobs","output_column":"name","output_value":"name"}],"formatZH":"修改计划任务 [name] 状态为 [status]","formatEN":"change the status of cronjob [name] to [status]."} +func (b *BaseApi) UpdateCronjobStatus(c *gin.Context) { + var req dto.CronjobUpdateStatus + if err := helper.CheckBindAndValidate(&req, c); err != nil { + return + } + + if err := cronjobService.UpdateStatus(req.ID, req.Status); err != nil { + helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) + return + } + helper.SuccessWithData(c, nil) +} + +// @Tags Cronjob +// @Summary Download cronjob records +// @Description 下载计划任务记录 +// @Accept json +// @Param request body dto.CronjobDownload true "request" +// @Success 200 +// @Security ApiKeyAuth +// @Router /cronjobs/download [post] +// @x-panel-log {"bodyKeys":["recordID"],"paramKeys":[],"BeforeFunctions":[{"input_column":"id","input_value":"recordID","isList":false,"db":"job_records","output_column":"file","output_value":"file"}],"formatZH":"下载计划任务记录 [file]","formatEN":"download the cronjob record [file]"} +func (b *BaseApi) TargetDownload(c *gin.Context) { + var req dto.CronjobDownload + if err := helper.CheckBindAndValidate(&req, c); err != nil { + return + } + + filePath, err := cronjobService.Download(req) + if err != nil { + helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) + return + } + + c.File(filePath) +} + +// @Tags Cronjob +// @Summary Handle cronjob once +// @Description 手动执行计划任务 +// @Accept json +// @Param request body dto.OperateByID true "request" +// @Success 200 +// @Security ApiKeyAuth +// @Router /cronjobs/handle [post] +// @x-panel-log {"bodyKeys":["id"],"paramKeys":[],"BeforeFunctions":[{"input_column":"id","input_value":"id","isList":false,"db":"cronjobs","output_column":"name","output_value":"name"}],"formatZH":"手动执行计划任务 [name]","formatEN":"manually execute the cronjob [name]"} +func (b *BaseApi) HandleOnce(c *gin.Context) { + var req dto.OperateByID + if err := helper.CheckBindAndValidate(&req, c); err != nil { + return + } + + if err := cronjobService.HandleOnce(req.ID); err != nil { + helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) + return + } + helper.SuccessWithData(c, nil) +} diff --git a/agent/app/api/v1/dashboard.go b/agent/app/api/v1/dashboard.go new file mode 100644 index 000000000..42e0d0360 --- /dev/null +++ b/agent/app/api/v1/dashboard.go @@ -0,0 +1,99 @@ +package v1 + +import ( + "errors" + + "github.com/1Panel-dev/1Panel/agent/app/api/v1/helper" + "github.com/1Panel-dev/1Panel/agent/constant" + "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 获取首页基础数据 +// @Accept json +// @Param ioOption path string true "request" +// @Param netOption path string true "request" +// @Success 200 {object} dto.DashboardBase +// @Security ApiKeyAuth +// @Router /dashboard/base/:ioOption/:netOption [get] +func (b *BaseApi) LoadDashboardBaseInfo(c *gin.Context) { + ioOption, ok := c.Params.Get("ioOption") + if !ok { + helper.ErrorWithDetail(c, constant.CodeErrBadRequest, constant.ErrTypeInvalidParams, errors.New("error ioOption in path")) + return + } + netOption, ok := c.Params.Get("netOption") + if !ok { + helper.ErrorWithDetail(c, constant.CodeErrBadRequest, constant.ErrTypeInvalidParams, errors.New("error netOption in path")) + return + } + data, err := dashboardService.LoadBaseInfo(ioOption, netOption) + if err != nil { + helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) + return + } + helper.SuccessWithData(c, data) +} + +// @Tags Dashboard +// @Summary Load dashboard current info +// @Description 获取首页实时数据 +// @Accept json +// @Param ioOption path string true "request" +// @Param netOption path string true "request" +// @Success 200 {object} dto.DashboardCurrent +// @Security ApiKeyAuth +// @Router /dashboard/current/:ioOption/:netOption [get] +func (b *BaseApi) LoadDashboardCurrentInfo(c *gin.Context) { + ioOption, ok := c.Params.Get("ioOption") + if !ok { + helper.ErrorWithDetail(c, constant.CodeErrBadRequest, constant.ErrTypeInvalidParams, errors.New("error ioOption in path")) + return + } + netOption, ok := c.Params.Get("netOption") + if !ok { + helper.ErrorWithDetail(c, constant.CodeErrBadRequest, constant.ErrTypeInvalidParams, errors.New("error netOption in path")) + return + } + + data := dashboardService.LoadCurrentInfo(ioOption, netOption) + helper.SuccessWithData(c, data) +} + +// @Tags Dashboard +// @Summary System restart +// @Description 重启服务器/面板 +// @Accept json +// @Param operation path string true "request" +// @Success 200 +// @Security ApiKeyAuth +// @Router /dashboard/system/restart/:operation [post] +func (b *BaseApi) SystemRestart(c *gin.Context) { + operation, ok := c.Params.Get("operation") + if !ok { + helper.ErrorWithDetail(c, constant.CodeErrBadRequest, constant.ErrTypeInvalidParams, errors.New("error operation in path")) + return + } + if err := dashboardService.Restart(operation); err != nil { + helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) + return + } + helper.SuccessWithData(c, nil) +} diff --git a/agent/app/api/v1/database.go b/agent/app/api/v1/database.go new file mode 100644 index 000000000..492a4801d --- /dev/null +++ b/agent/app/api/v1/database.go @@ -0,0 +1,229 @@ +package v1 + +import ( + "encoding/base64" + + "github.com/1Panel-dev/1Panel/agent/app/api/v1/helper" + "github.com/1Panel-dev/1Panel/agent/app/dto" + "github.com/1Panel-dev/1Panel/agent/constant" + "github.com/gin-gonic/gin" +) + +// @Tags Database +// @Summary Create database +// @Description 创建远程数据库 +// @Accept json +// @Param request body dto.DatabaseCreate true "request" +// @Success 200 +// @Security ApiKeyAuth +// @Router /databases/db [post] +// @x-panel-log {"bodyKeys":["name", "type"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"创建远程数据库 [name][type]","formatEN":"create database [name][type]"} +func (b *BaseApi) CreateDatabase(c *gin.Context) { + var req dto.DatabaseCreate + if err := helper.CheckBindAndValidate(&req, c); err != nil { + return + } + if req.SSL { + key, _ := base64.StdEncoding.DecodeString(req.ClientKey) + req.ClientKey = string(key) + cert, _ := base64.StdEncoding.DecodeString(req.ClientCert) + req.ClientCert = string(cert) + ca, _ := base64.StdEncoding.DecodeString(req.RootCert) + req.RootCert = string(ca) + } + + if err := databaseService.Create(req); err != nil { + helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) + return + } + helper.SuccessWithData(c, nil) +} + +// @Tags Database +// @Summary Check database +// @Description 检测远程数据库连接性 +// @Accept json +// @Param request body dto.DatabaseCreate true "request" +// @Success 200 +// @Security ApiKeyAuth +// @Router /databases/db/check [post] +// @x-panel-log {"bodyKeys":["name", "type"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"检测远程数据库 [name][type] 连接性","formatEN":"check if database [name][type] is connectable"} +func (b *BaseApi) CheckDatabase(c *gin.Context) { + var req dto.DatabaseCreate + if err := helper.CheckBindAndValidate(&req, c); err != nil { + return + } + if req.SSL { + clientKey, _ := base64.StdEncoding.DecodeString(req.ClientKey) + req.ClientKey = string(clientKey) + clientCert, _ := base64.StdEncoding.DecodeString(req.ClientCert) + req.ClientCert = string(clientCert) + rootCert, _ := base64.StdEncoding.DecodeString(req.RootCert) + req.RootCert = string(rootCert) + } + + helper.SuccessWithData(c, databaseService.CheckDatabase(req)) +} + +// @Tags Database +// @Summary Page databases +// @Description 获取远程数据库列表分页 +// @Accept json +// @Param request body dto.DatabaseSearch true "request" +// @Success 200 {object} dto.PageResult +// @Security ApiKeyAuth +// @Router /databases/db/search [post] +func (b *BaseApi) SearchDatabase(c *gin.Context) { + var req dto.DatabaseSearch + if err := helper.CheckBindAndValidate(&req, c); err != nil { + return + } + + total, list, err := databaseService.SearchWithPage(req) + if err != nil { + helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) + return + } + + helper.SuccessWithData(c, dto.PageResult{ + Items: list, + Total: total, + }) +} + +// @Tags Database +// @Summary List databases +// @Description 获取远程数据库列表 +// @Success 200 {array} dto.DatabaseOption +// @Security ApiKeyAuth +// @Router /databases/db/list/:type [get] +func (b *BaseApi) ListDatabase(c *gin.Context) { + dbType, err := helper.GetStrParamByKey(c, "type") + if err != nil { + helper.ErrorWithDetail(c, constant.CodeErrBadRequest, constant.ErrTypeInvalidParams, err) + return + } + list, err := databaseService.List(dbType) + if err != nil { + helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) + return + } + + helper.SuccessWithData(c, list) +} + +// @Tags Database +// @Summary List databases +// @Description 获取数据库列表 +// @Success 200 {array} dto.DatabaseItem +// @Security ApiKeyAuth +// @Router /databases/db/item/:type [get] +func (b *BaseApi) LoadDatabaseItems(c *gin.Context) { + dbType, err := helper.GetStrParamByKey(c, "type") + if err != nil { + helper.ErrorWithDetail(c, constant.CodeErrBadRequest, constant.ErrTypeInvalidParams, err) + return + } + list, err := databaseService.LoadItems(dbType) + if err != nil { + helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) + return + } + + helper.SuccessWithData(c, list) +} + +// @Tags Database +// @Summary Get databases +// @Description 获取远程数据库 +// @Success 200 {object} dto.DatabaseInfo +// @Security ApiKeyAuth +// @Router /databases/db/:name [get] +func (b *BaseApi) GetDatabase(c *gin.Context) { + name, err := helper.GetStrParamByKey(c, "name") + if err != nil { + helper.ErrorWithDetail(c, constant.CodeErrBadRequest, constant.ErrTypeInvalidParams, err) + return + } + data, err := databaseService.Get(name) + if err != nil { + helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) + return + } + + helper.SuccessWithData(c, data) +} + +// @Tags Database +// @Summary Check before delete remote database +// @Description Mysql 远程数据库删除前检查 +// @Accept json +// @Param request body dto.OperateByID true "request" +// @Success 200 {array} string +// @Security ApiKeyAuth +// @Router /db/remote/del/check [post] +func (b *BaseApi) DeleteCheckDatabase(c *gin.Context) { + var req dto.OperateByID + if err := helper.CheckBindAndValidate(&req, c); err != nil { + return + } + + apps, err := databaseService.DeleteCheck(req.ID) + if err != nil { + helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) + return + } + helper.SuccessWithData(c, apps) +} + +// @Tags Database +// @Summary Delete database +// @Description 删除远程数据库 +// @Accept json +// @Param request body dto.DatabaseDelete true "request" +// @Success 200 +// @Security ApiKeyAuth +// @Router /databases/db/del [post] +// @x-panel-log {"bodyKeys":["ids"],"paramKeys":[],"BeforeFunctions":[{"input_column":"id","input_value":"ids","isList":true,"db":"databases","output_column":"name","output_value":"names"}],"formatZH":"删除远程数据库 [names]","formatEN":"delete database [names]"} +func (b *BaseApi) DeleteDatabase(c *gin.Context) { + var req dto.DatabaseDelete + if err := helper.CheckBindAndValidate(&req, c); err != nil { + return + } + + if err := databaseService.Delete(req); err != nil { + helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) + return + } + helper.SuccessWithData(c, nil) +} + +// @Tags Database +// @Summary Update database +// @Description 更新远程数据库 +// @Accept json +// @Param request body dto.DatabaseUpdate true "request" +// @Success 200 +// @Security ApiKeyAuth +// @Router /databases/db/update [post] +// @x-panel-log {"bodyKeys":["name"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"更新远程数据库 [name]","formatEN":"update database [name]"} +func (b *BaseApi) UpdateDatabase(c *gin.Context) { + var req dto.DatabaseUpdate + if err := helper.CheckBindAndValidate(&req, c); err != nil { + return + } + if req.SSL { + cKey, _ := base64.StdEncoding.DecodeString(req.ClientKey) + req.ClientKey = string(cKey) + cCert, _ := base64.StdEncoding.DecodeString(req.ClientCert) + req.ClientCert = string(cCert) + ca, _ := base64.StdEncoding.DecodeString(req.RootCert) + req.RootCert = string(ca) + } + + if err := databaseService.Update(req); err != nil { + helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) + return + } + helper.SuccessWithData(c, nil) +} diff --git a/agent/app/api/v1/database_common.go b/agent/app/api/v1/database_common.go new file mode 100644 index 000000000..0ed2adc94 --- /dev/null +++ b/agent/app/api/v1/database_common.go @@ -0,0 +1,75 @@ +package v1 + +import ( + "github.com/1Panel-dev/1Panel/agent/app/api/v1/helper" + "github.com/1Panel-dev/1Panel/agent/app/dto" + "github.com/1Panel-dev/1Panel/agent/constant" + "github.com/gin-gonic/gin" +) + +// @Tags Database Common +// @Summary Load base info +// @Description 获取数据库基础信息 +// @Accept json +// @Param request body dto.OperationWithNameAndType true "request" +// @Success 200 {object} dto.DBBaseInfo +// @Security ApiKeyAuth +// @Router /databases/common/info [post] +func (b *BaseApi) LoadDBBaseInfo(c *gin.Context) { + var req dto.OperationWithNameAndType + if err := helper.CheckBindAndValidate(&req, c); err != nil { + return + } + + data, err := dbCommonService.LoadBaseInfo(req) + if err != nil { + helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) + return + } + + helper.SuccessWithData(c, data) +} + +// @Tags Database Common +// @Summary Load Database conf +// @Description 获取数据库配置文件 +// @Accept json +// @Param request body dto.OperationWithNameAndType true "request" +// @Success 200 +// @Security ApiKeyAuth +// @Router /databases/common/load/file [post] +func (b *BaseApi) LoadDBFile(c *gin.Context) { + var req dto.OperationWithNameAndType + if err := helper.CheckBindAndValidate(&req, c); err != nil { + return + } + content, err := dbCommonService.LoadDatabaseFile(req) + if err != nil { + helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) + return + } + helper.SuccessWithData(c, content) +} + +// @Tags Database Common +// @Summary Update conf by upload file +// @Description 上传替换配置文件 +// @Accept json +// @Param request body dto.DBConfUpdateByFile true "request" +// @Success 200 +// @Security ApiKeyAuth +// @Router /databases/common/update/conf [post] +// @x-panel-log {"bodyKeys":["type","database"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"更新 [type] 数据库 [database] 配置信息","formatEN":"update the [type] [database] database configuration information"} +func (b *BaseApi) UpdateDBConfByFile(c *gin.Context) { + var req dto.DBConfUpdateByFile + if err := helper.CheckBindAndValidate(&req, c); err != nil { + return + } + + if err := dbCommonService.UpdateConfByFile(req); err != nil { + helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) + return + } + + helper.SuccessWithData(c, nil) +} diff --git a/agent/app/api/v1/database_mysql.go b/agent/app/api/v1/database_mysql.go new file mode 100644 index 000000000..db3feda7d --- /dev/null +++ b/agent/app/api/v1/database_mysql.go @@ -0,0 +1,350 @@ +package v1 + +import ( + "context" + "encoding/base64" + + "github.com/1Panel-dev/1Panel/agent/app/api/v1/helper" + "github.com/1Panel-dev/1Panel/agent/app/dto" + "github.com/1Panel-dev/1Panel/agent/constant" + "github.com/gin-gonic/gin" +) + +// @Tags Database Mysql +// @Summary Create mysql database +// @Description 创建 mysql 数据库 +// @Accept json +// @Param request body dto.MysqlDBCreate true "request" +// @Success 200 +// @Security ApiKeyAuth +// @Router /databases [post] +// @x-panel-log {"bodyKeys":["name"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"创建 mysql 数据库 [name]","formatEN":"create mysql database [name]"} +func (b *BaseApi) CreateMysql(c *gin.Context) { + var req dto.MysqlDBCreate + if err := helper.CheckBindAndValidate(&req, c); err != nil { + return + } + + if len(req.Password) != 0 { + password, err := base64.StdEncoding.DecodeString(req.Password) + if err != nil { + helper.ErrorWithDetail(c, constant.CodeErrBadRequest, constant.ErrTypeInvalidParams, err) + return + } + req.Password = string(password) + } + + if _, err := mysqlService.Create(context.Background(), req); err != nil { + helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) + return + } + helper.SuccessWithData(c, nil) +} + +// @Tags Database Mysql +// @Summary Bind user of mysql database +// @Description 绑定 mysql 数据库用户 +// @Accept json +// @Param request body dto.BindUser true "request" +// @Success 200 +// @Security ApiKeyAuth +// @Router /databases/bind [post] +// @x-panel-log {"bodyKeys":["database", "username"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"绑定 mysql 数据库名 [database] [username]","formatEN":"bind mysql database [database] [username]"} +func (b *BaseApi) BindUser(c *gin.Context) { + var req dto.BindUser + if err := helper.CheckBindAndValidate(&req, c); err != nil { + return + } + + if len(req.Password) != 0 { + password, err := base64.StdEncoding.DecodeString(req.Password) + if err != nil { + helper.ErrorWithDetail(c, constant.CodeErrBadRequest, constant.ErrTypeInvalidParams, err) + return + } + req.Password = string(password) + } + + if err := mysqlService.BindUser(req); err != nil { + helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) + return + } + helper.SuccessWithData(c, nil) +} + +// @Tags Database Mysql +// @Summary Update mysql database description +// @Description 更新 mysql 数据库库描述信息 +// @Accept json +// @Param request body dto.UpdateDescription true "request" +// @Success 200 +// @Security ApiKeyAuth +// @Router /databases/description/update [post] +// @x-panel-log {"bodyKeys":["id","description"],"paramKeys":[],"BeforeFunctions":[{"input_column":"id","input_value":"id","isList":false,"db":"database_mysqls","output_column":"name","output_value":"name"}],"formatZH":"mysql 数据库 [name] 描述信息修改 [description]","formatEN":"The description of the mysql database [name] is modified => [description]"} +func (b *BaseApi) UpdateMysqlDescription(c *gin.Context) { + var req dto.UpdateDescription + if err := helper.CheckBindAndValidate(&req, c); err != nil { + return + } + + if err := mysqlService.UpdateDescription(req); err != nil { + helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) + return + } + helper.SuccessWithData(c, nil) +} + +// @Tags Database Mysql +// @Summary Change mysql password +// @Description 修改 mysql 密码 +// @Accept json +// @Param request body dto.ChangeDBInfo true "request" +// @Success 200 +// @Security ApiKeyAuth +// @Router /databases/change/password [post] +// @x-panel-log {"bodyKeys":["id"],"paramKeys":[],"BeforeFunctions":[{"input_column":"id","input_value":"id","isList":false,"db":"database_mysqls","output_column":"name","output_value":"name"}],"formatZH":"更新数据库 [name] 密码","formatEN":"Update database [name] password"} +func (b *BaseApi) ChangeMysqlPassword(c *gin.Context) { + var req dto.ChangeDBInfo + if err := helper.CheckBindAndValidate(&req, c); err != nil { + return + } + + if len(req.Value) != 0 { + value, err := base64.StdEncoding.DecodeString(req.Value) + if err != nil { + helper.ErrorWithDetail(c, constant.CodeErrBadRequest, constant.ErrTypeInvalidParams, err) + return + } + req.Value = string(value) + } + + if err := mysqlService.ChangePassword(req); err != nil { + helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) + return + } + helper.SuccessWithData(c, nil) +} + +// @Tags Database Mysql +// @Summary Change mysql access +// @Description 修改 mysql 访问权限 +// @Accept json +// @Param request body dto.ChangeDBInfo true "request" +// @Success 200 +// @Security ApiKeyAuth +// @Router /databases/change/access [post] +// @x-panel-log {"bodyKeys":["id"],"paramKeys":[],"BeforeFunctions":[{"input_column":"id","input_value":"id","isList":false,"db":"database_mysqls","output_column":"name","output_value":"name"}],"formatZH":"更新数据库 [name] 访问权限","formatEN":"Update database [name] access"} +func (b *BaseApi) ChangeMysqlAccess(c *gin.Context) { + var req dto.ChangeDBInfo + if err := helper.CheckBindAndValidate(&req, c); err != nil { + return + } + + if err := mysqlService.ChangeAccess(req); err != nil { + helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) + return + } + helper.SuccessWithData(c, nil) +} + +// @Tags Database Mysql +// @Summary Update mysql variables +// @Description mysql 性能调优 +// @Accept json +// @Param request body dto.MysqlVariablesUpdate true "request" +// @Success 200 +// @Security ApiKeyAuth +// @Router /databases/variables/update [post] +// @x-panel-log {"bodyKeys":[],"paramKeys":[],"BeforeFunctions":[],"formatZH":"调整 mysql 数据库性能参数","formatEN":"adjust mysql database performance parameters"} +func (b *BaseApi) UpdateMysqlVariables(c *gin.Context) { + var req dto.MysqlVariablesUpdate + if err := helper.CheckBindAndValidate(&req, c); err != nil { + return + } + + if err := mysqlService.UpdateVariables(req); err != nil { + helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) + return + } + helper.SuccessWithData(c, nil) +} + +// @Tags Database Mysql +// @Summary Page mysql databases +// @Description 获取 mysql 数据库列表分页 +// @Accept json +// @Param request body dto.MysqlDBSearch true "request" +// @Success 200 {object} dto.PageResult +// @Security ApiKeyAuth +// @Router /databases/search [post] +func (b *BaseApi) SearchMysql(c *gin.Context) { + var req dto.MysqlDBSearch + if err := helper.CheckBindAndValidate(&req, c); err != nil { + return + } + + total, list, err := mysqlService.SearchWithPage(req) + if err != nil { + helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) + return + } + + helper.SuccessWithData(c, dto.PageResult{ + Items: list, + Total: total, + }) +} + +// @Tags Database Mysql +// @Summary List mysql database names +// @Description 获取 mysql 数据库列表 +// @Accept json +// @Param request body dto.PageInfo true "request" +// @Success 200 {array} dto.MysqlOption +// @Security ApiKeyAuth +// @Router /databases/options [get] +func (b *BaseApi) ListDBName(c *gin.Context) { + list, err := mysqlService.ListDBOption() + if err != nil { + helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) + return + } + + helper.SuccessWithData(c, list) +} + +// @Tags Database Mysql +// @Summary Load mysql database from remote +// @Description 从服务器获取 +// @Accept json +// @Param request body dto.MysqlLoadDB true "request" +// @Security ApiKeyAuth +// @Router /databases/load [post] +func (b *BaseApi) LoadDBFromRemote(c *gin.Context) { + var req dto.MysqlLoadDB + if err := helper.CheckBindAndValidate(&req, c); err != nil { + return + } + + if err := mysqlService.LoadFromRemote(req); err != nil { + helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) + return + } + + helper.SuccessWithData(c, nil) +} + +// @Tags Database Mysql +// @Summary Check before delete mysql database +// @Description Mysql 数据库删除前检查 +// @Accept json +// @Param request body dto.MysqlDBDeleteCheck true "request" +// @Success 200 {array} string +// @Security ApiKeyAuth +// @Router /databases/del/check [post] +func (b *BaseApi) DeleteCheckMysql(c *gin.Context) { + var req dto.MysqlDBDeleteCheck + if err := helper.CheckBindAndValidate(&req, c); err != nil { + return + } + + apps, err := mysqlService.DeleteCheck(req) + if err != nil { + helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) + return + } + helper.SuccessWithData(c, apps) +} + +// @Tags Database Mysql +// @Summary Delete mysql database +// @Description 删除 mysql 数据库 +// @Accept json +// @Param request body dto.MysqlDBDelete true "request" +// @Success 200 +// @Security ApiKeyAuth +// @Router /databases/del [post] +// @x-panel-log {"bodyKeys":["id"],"paramKeys":[],"BeforeFunctions":[{"input_column":"id","input_value":"id","isList":false,"db":"database_mysqls","output_column":"name","output_value":"name"}],"formatZH":"删除 mysql 数据库 [name]","formatEN":"delete mysql database [name]"} +func (b *BaseApi) DeleteMysql(c *gin.Context) { + var req dto.MysqlDBDelete + if err := helper.CheckBindAndValidate(&req, c); err != nil { + return + } + + tx, ctx := helper.GetTxAndContext() + if err := mysqlService.Delete(ctx, req); err != nil { + helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) + tx.Rollback() + return + } + tx.Commit() + helper.SuccessWithData(c, nil) +} + +// @Tags Database Mysql +// @Summary Load mysql remote access +// @Description 获取 mysql 远程访问权限 +// @Accept json +// @Param request body dto.OperationWithNameAndType true "request" +// @Success 200 {boolean} isRemote +// @Security ApiKeyAuth +// @Router /databases/remote [post] +func (b *BaseApi) LoadRemoteAccess(c *gin.Context) { + var req dto.OperationWithNameAndType + if err := helper.CheckBindAndValidate(&req, c); err != nil { + return + } + isRemote, err := mysqlService.LoadRemoteAccess(req) + if err != nil { + helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) + return + } + + helper.SuccessWithData(c, isRemote) +} + +// @Tags Database Mysql +// @Summary Load mysql status info +// @Description 获取 mysql 状态信息 +// @Accept json +// @Param request body dto.OperationWithNameAndType true "request" +// @Success 200 {object} dto.MysqlStatus +// @Security ApiKeyAuth +// @Router /databases/status [post] +func (b *BaseApi) LoadStatus(c *gin.Context) { + var req dto.OperationWithNameAndType + if err := helper.CheckBindAndValidate(&req, c); err != nil { + return + } + + data, err := mysqlService.LoadStatus(req) + if err != nil { + helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) + return + } + + helper.SuccessWithData(c, data) +} + +// @Tags Database Mysql +// @Summary Load mysql variables info +// @Description 获取 mysql 性能参数信息 +// @Accept json +// @Param request body dto.OperationWithNameAndType true "request" +// @Success 200 {object} dto.MysqlVariables +// @Security ApiKeyAuth +// @Router /databases/variables [post] +func (b *BaseApi) LoadVariables(c *gin.Context) { + var req dto.OperationWithNameAndType + if err := helper.CheckBindAndValidate(&req, c); err != nil { + return + } + + data, err := mysqlService.LoadVariables(req) + if err != nil { + helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) + return + } + + helper.SuccessWithData(c, data) +} diff --git a/agent/app/api/v1/database_postgresql.go b/agent/app/api/v1/database_postgresql.go new file mode 100644 index 000000000..53f45e267 --- /dev/null +++ b/agent/app/api/v1/database_postgresql.go @@ -0,0 +1,233 @@ +package v1 + +import ( + "context" + "encoding/base64" + + "github.com/1Panel-dev/1Panel/agent/app/api/v1/helper" + "github.com/1Panel-dev/1Panel/agent/app/dto" + "github.com/1Panel-dev/1Panel/agent/constant" + "github.com/gin-gonic/gin" +) + +// @Tags Database Postgresql +// @Summary Create postgresql database +// @Description 创建 postgresql 数据库 +// @Accept json +// @Param request body dto.PostgresqlDBCreate true "request" +// @Success 200 +// @Security ApiKeyAuth +// @Router /databases/pg [post] +// @x-panel-log {"bodyKeys":["name"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"创建 postgresql 数据库 [name]","formatEN":"create postgresql database [name]"} +func (b *BaseApi) CreatePostgresql(c *gin.Context) { + var req dto.PostgresqlDBCreate + if err := helper.CheckBindAndValidate(&req, c); err != nil { + return + } + + if len(req.Password) != 0 { + password, err := base64.StdEncoding.DecodeString(req.Password) + if err != nil { + helper.ErrorWithDetail(c, constant.CodeErrBadRequest, constant.ErrTypeInvalidParams, err) + return + } + req.Password = string(password) + } + + if _, err := postgresqlService.Create(context.Background(), req); err != nil { + helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) + return + } + helper.SuccessWithData(c, nil) +} + +// @Tags Database Postgresql +// @Summary Bind postgresql user +// @Description 绑定 postgresql 数据库用户 +// @Accept json +// @Param request body dto.PostgresqlBindUser true "request" +// @Success 200 +// @Security ApiKeyAuth +// @Router /databases/pg/bind [post] +// @x-panel-log {"bodyKeys":["name", "username"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"绑定 postgresql 数据库 [name] 用户 [username]","formatEN":"bind postgresql database [name] user [username]"} +func (b *BaseApi) BindPostgresqlUser(c *gin.Context) { + var req dto.PostgresqlBindUser + if err := helper.CheckBindAndValidate(&req, c); err != nil { + return + } + + if err := postgresqlService.BindUser(req); err != nil { + helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) + return + } + helper.SuccessWithData(c, nil) +} + +// @Tags Database Postgresql +// @Summary Update postgresql database description +// @Description 更新 postgresql 数据库库描述信息 +// @Accept json +// @Param request body dto.UpdateDescription true "request" +// @Success 200 +// @Security ApiKeyAuth +// @Router /databases/pg/description [post] +// @x-panel-log {"bodyKeys":["id","description"],"paramKeys":[],"BeforeFunctions":[{"input_column":"id","input_value":"id","isList":false,"db":"database_postgresqls","output_column":"name","output_value":"name"}],"formatZH":"postgresql 数据库 [name] 描述信息修改 [description]","formatEN":"The description of the postgresql database [name] is modified => [description]"} +func (b *BaseApi) UpdatePostgresqlDescription(c *gin.Context) { + var req dto.UpdateDescription + if err := helper.CheckBindAndValidate(&req, c); err != nil { + return + } + + if err := postgresqlService.UpdateDescription(req); err != nil { + helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) + return + } + helper.SuccessWithData(c, nil) +} + +// @Tags Database Postgresql +// @Summary Change postgresql privileges +// @Description 修改 postgresql 用户权限 +// @Accept json +// @Param request body dto.ChangeDBInfo true "request" +// @Success 200 +// @Security ApiKeyAuth +// @Router /databases/pg/privileges [post] +// @x-panel-log {"bodyKeys":["database", "username"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"更新数据库 [database] 用户 [username] 权限","formatEN":"Update [user] privileges of database [database]"} +func (b *BaseApi) ChangePostgresqlPrivileges(c *gin.Context) { + var req dto.PostgresqlPrivileges + if err := helper.CheckBindAndValidate(&req, c); err != nil { + return + } + + if err := postgresqlService.ChangePrivileges(req); err != nil { + helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) + return + } + helper.SuccessWithData(c, nil) +} + +// @Tags Database Postgresql +// @Summary Change postgresql password +// @Description 修改 postgresql 密码 +// @Accept json +// @Param request body dto.ChangeDBInfo true "request" +// @Success 200 +// @Security ApiKeyAuth +// @Router /databases/pg/password [post] +// @x-panel-log {"bodyKeys":["id"],"paramKeys":[],"BeforeFunctions":[{"input_column":"id","input_value":"id","isList":false,"db":"database_postgresqls","output_column":"name","output_value":"name"}],"formatZH":"更新数据库 [name] 密码","formatEN":"Update database [name] password"} +func (b *BaseApi) ChangePostgresqlPassword(c *gin.Context) { + var req dto.ChangeDBInfo + if err := helper.CheckBindAndValidate(&req, c); err != nil { + return + } + + if len(req.Value) != 0 { + value, err := base64.StdEncoding.DecodeString(req.Value) + if err != nil { + helper.ErrorWithDetail(c, constant.CodeErrBadRequest, constant.ErrTypeInvalidParams, err) + return + } + req.Value = string(value) + } + + if err := postgresqlService.ChangePassword(req); err != nil { + helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) + return + } + helper.SuccessWithData(c, nil) +} + +// @Tags Database Postgresql +// @Summary Page postgresql databases +// @Description 获取 postgresql 数据库列表分页 +// @Accept json +// @Param request body dto.PostgresqlDBSearch true "request" +// @Success 200 {object} dto.PageResult +// @Security ApiKeyAuth +// @Router /databases/pg/search [post] +func (b *BaseApi) SearchPostgresql(c *gin.Context) { + var req dto.PostgresqlDBSearch + if err := helper.CheckBindAndValidate(&req, c); err != nil { + return + } + + total, list, err := postgresqlService.SearchWithPage(req) + if err != nil { + helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) + return + } + + helper.SuccessWithData(c, dto.PageResult{ + Items: list, + Total: total, + }) +} + +// @Tags Database Postgresql +// @Summary Load postgresql database from remote +// @Description 从服务器获取 +// @Accept json +// @Param request body dto.PostgresqlLoadDB true "request" +// @Security ApiKeyAuth +// @Router /databases/pg/:database/load [post] +func (b *BaseApi) LoadPostgresqlDBFromRemote(c *gin.Context) { + database, err := helper.GetStrParamByKey(c, "database") + if err != nil { + helper.ErrorWithDetail(c, constant.CodeErrBadRequest, constant.ErrTypeInvalidParams, err) + return + } + + if err := postgresqlService.LoadFromRemote(database); err != nil { + helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) + return + } + + helper.SuccessWithData(c, nil) +} + +// @Tags Database Postgresql +// @Summary Check before delete postgresql database +// @Description Postgresql 数据库删除前检查 +// @Accept json +// @Param request body dto.PostgresqlDBDeleteCheck true "request" +// @Success 200 {array} string +// @Security ApiKeyAuth +// @Router /databases/pg/del/check [post] +func (b *BaseApi) DeleteCheckPostgresql(c *gin.Context) { + var req dto.PostgresqlDBDeleteCheck + if err := helper.CheckBindAndValidate(&req, c); err != nil { + return + } + + apps, err := postgresqlService.DeleteCheck(req) + if err != nil { + helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) + return + } + helper.SuccessWithData(c, apps) +} + +// @Tags Database Postgresql +// @Summary Delete postgresql database +// @Description 删除 postgresql 数据库 +// @Accept json +// @Param request body dto.PostgresqlDBDelete true "request" +// @Success 200 +// @Security ApiKeyAuth +// @Router /databases/pg/del [post] +// @x-panel-log {"bodyKeys":["id"],"paramKeys":[],"BeforeFunctions":[{"input_column":"id","input_value":"id","isList":false,"db":"database_postgresqls","output_column":"name","output_value":"name"}],"formatZH":"删除 postgresql 数据库 [name]","formatEN":"delete postgresql database [name]"} +func (b *BaseApi) DeletePostgresql(c *gin.Context) { + var req dto.PostgresqlDBDelete + if err := helper.CheckBindAndValidate(&req, c); err != nil { + return + } + tx, ctx := helper.GetTxAndContext() + if err := postgresqlService.Delete(ctx, req); err != nil { + helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) + tx.Rollback() + return + } + tx.Commit() + helper.SuccessWithData(c, nil) +} diff --git a/agent/app/api/v1/database_redis.go b/agent/app/api/v1/database_redis.go new file mode 100644 index 000000000..8483b89ed --- /dev/null +++ b/agent/app/api/v1/database_redis.go @@ -0,0 +1,170 @@ +package v1 + +import ( + "encoding/base64" + + "github.com/1Panel-dev/1Panel/agent/app/api/v1/helper" + "github.com/1Panel-dev/1Panel/agent/app/dto" + "github.com/1Panel-dev/1Panel/agent/constant" + "github.com/gin-gonic/gin" +) + +// @Tags Database Redis +// @Summary Load redis status info +// @Description 获取 redis 状态信息 +// @Accept json +// @Param request body dto.OperationWithName true "request" +// @Success 200 {object} dto.RedisStatus +// @Security ApiKeyAuth +// @Router /databases/redis/status [post] +func (b *BaseApi) LoadRedisStatus(c *gin.Context) { + var req dto.OperationWithName + if err := helper.CheckBind(&req, c); err != nil { + return + } + data, err := redisService.LoadStatus(req) + if err != nil { + helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) + return + } + + helper.SuccessWithData(c, data) +} + +// @Tags Database Redis +// @Summary Load redis conf +// @Description 获取 redis 配置信息 +// @Accept json +// @Param request body dto.OperationWithName true "request" +// @Success 200 {object} dto.RedisConf +// @Security ApiKeyAuth +// @Router /databases/redis/conf [post] +func (b *BaseApi) LoadRedisConf(c *gin.Context) { + var req dto.OperationWithName + if err := helper.CheckBind(&req, c); err != nil { + return + } + data, err := redisService.LoadConf(req) + if err != nil { + helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) + return + } + + helper.SuccessWithData(c, data) +} + +// @Tags Database Redis +// @Summary Load redis persistence conf +// @Description 获取 redis 持久化配置 +// @Accept json +// @Param request body dto.OperationWithName true "request" +// @Success 200 {object} dto.RedisPersistence +// @Security ApiKeyAuth +// @Router /databases/redis/persistence/conf [post] +func (b *BaseApi) LoadPersistenceConf(c *gin.Context) { + var req dto.OperationWithName + if err := helper.CheckBind(&req, c); err != nil { + return + } + data, err := redisService.LoadPersistenceConf(req) + if err != nil { + helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) + return + } + + helper.SuccessWithData(c, data) +} + +func (b *BaseApi) CheckHasCli(c *gin.Context) { + helper.SuccessWithData(c, redisService.CheckHasCli()) +} + +// @Tags Database Redis +// @Summary Install redis-cli +// @Description 安装 redis cli +// @Success 200 +// @Security ApiKeyAuth +// @Router /databases/redis/install/cli [post] +func (b *BaseApi) InstallCli(c *gin.Context) { + if err := redisService.InstallCli(); err != nil { + helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) + return + } + + helper.SuccessWithOutData(c) +} + +// @Tags Database Redis +// @Summary Update redis conf +// @Description 更新 redis 配置信息 +// @Accept json +// @Param request body dto.RedisConfUpdate true "request" +// @Success 200 +// @Security ApiKeyAuth +// @Router /databases/redis/conf/update [post] +// @x-panel-log {"bodyKeys":[],"paramKeys":[],"BeforeFunctions":[],"formatZH":"更新 redis 数据库配置信息","formatEN":"update the redis database configuration information"} +func (b *BaseApi) UpdateRedisConf(c *gin.Context) { + var req dto.RedisConfUpdate + if err := helper.CheckBind(&req, c); err != nil { + return + } + + if err := redisService.UpdateConf(req); err != nil { + helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) + return + } + helper.SuccessWithData(c, nil) +} + +// @Tags Database Redis +// @Summary Change redis password +// @Description 更新 redis 密码 +// @Accept json +// @Param request body dto.ChangeRedisPass true "request" +// @Success 200 +// @Security ApiKeyAuth +// @Router /databases/redis/password [post] +// @x-panel-log {"bodyKeys":[],"paramKeys":[],"BeforeFunctions":[],"formatZH":"修改 redis 数据库密码","formatEN":"change the password of the redis database"} +func (b *BaseApi) ChangeRedisPassword(c *gin.Context) { + var req dto.ChangeRedisPass + if err := helper.CheckBindAndValidate(&req, c); err != nil { + return + } + + if len(req.Value) != 0 { + value, err := base64.StdEncoding.DecodeString(req.Value) + if err != nil { + helper.ErrorWithDetail(c, constant.CodeErrBadRequest, constant.ErrTypeInvalidParams, err) + return + } + req.Value = string(value) + } + + if err := redisService.ChangePassword(req); err != nil { + helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) + return + } + helper.SuccessWithData(c, nil) +} + +// @Tags Database Redis +// @Summary Update redis persistence conf +// @Description 更新 redis 持久化配置 +// @Accept json +// @Param request body dto.RedisConfPersistenceUpdate true "request" +// @Success 200 +// @Security ApiKeyAuth +// @Router /databases/redis/persistence/update [post] +// @x-panel-log {"bodyKeys":[],"paramKeys":[],"BeforeFunctions":[],"formatZH":"redis 数据库持久化配置更新","formatEN":"redis database persistence configuration update"} +func (b *BaseApi) UpdateRedisPersistenceConf(c *gin.Context) { + var req dto.RedisConfPersistenceUpdate + if err := helper.CheckBindAndValidate(&req, c); err != nil { + return + } + + if err := redisService.UpdatePersistenceConf(req); err != nil { + helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) + return + } + helper.SuccessWithData(c, nil) +} diff --git a/agent/app/api/v1/device.go b/agent/app/api/v1/device.go new file mode 100644 index 000000000..0216465a4 --- /dev/null +++ b/agent/app/api/v1/device.go @@ -0,0 +1,236 @@ +package v1 + +import ( + "encoding/base64" + + "github.com/1Panel-dev/1Panel/agent/app/api/v1/helper" + "github.com/1Panel-dev/1Panel/agent/app/dto" + "github.com/1Panel-dev/1Panel/agent/constant" + "github.com/gin-gonic/gin" +) + +// @Tags Device +// @Summary Load device base info +// @Description 获取设备基础信息 +// @Success 200 {object} dto.DeviceBaseInfo +// @Security ApiKeyAuth +// @Router /toolbox/device/base [post] +func (b *BaseApi) LoadDeviceBaseInfo(c *gin.Context) { + data, err := deviceService.LoadBaseInfo() + if err != nil { + helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) + return + } + + helper.SuccessWithData(c, data) +} + +// @Tags Device +// @Summary list time zone options +// @Description 获取系统可用时区选项 +// @Accept json +// @Success 200 {Array} string +// @Security ApiKeyAuth +// @Router /toolbox/device/zone/options [get] +func (b *BaseApi) LoadTimeOption(c *gin.Context) { + list, err := deviceService.LoadTimeZone() + if err != nil { + helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) + return + } + + helper.SuccessWithData(c, list) +} + +// @Tags Device +// @Summary load conf +// @Description 获取系统配置文件 +// @Accept json +// @Param request body dto.OperationWithName true "request" +// @Success 200 +// @Security ApiKeyAuth +// @Router /toolbox/device/conf [post] +func (b *BaseApi) LoadDeviceConf(c *gin.Context) { + var req dto.OperationWithName + if err := helper.CheckBindAndValidate(&req, c); err != nil { + return + } + + list, err := deviceService.LoadConf(req.Name) + if err != nil { + helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) + return + } + + helper.SuccessWithData(c, list) +} + +// @Tags Device +// @Summary Update device conf by file +// @Description 通过文件修改配置 +// @Accept json +// @Param request body dto.UpdateByNameAndFile true "request" +// @Success 200 +// @Security ApiKeyAuth +// @Router /toolbox/device/update/byconf [post] +func (b *BaseApi) UpdateDeviceByFile(c *gin.Context) { + var req dto.UpdateByNameAndFile + if err := helper.CheckBindAndValidate(&req, c); err != nil { + return + } + if err := deviceService.UpdateByConf(req); err != nil { + helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) + return + } + + helper.SuccessWithData(c, nil) +} + +// @Tags Device +// @Summary Update device +// @Description 修改系统参数 +// @Accept json +// @Param request body dto.SettingUpdate true "request" +// @Success 200 +// @Security ApiKeyAuth +// @Router /toolbox/device/update/conf [post] +// @x-panel-log {"bodyKeys":["key","value"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"修改主机参数 [key] => [value]","formatEN":"update device conf [key] => [value]"} +func (b *BaseApi) UpdateDeviceConf(c *gin.Context) { + var req dto.SettingUpdate + if err := helper.CheckBindAndValidate(&req, c); err != nil { + return + } + + if err := deviceService.Update(req.Key, req.Value); err != nil { + helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) + return + } + + helper.SuccessWithData(c, nil) +} + +// @Tags Device +// @Summary Update device hosts +// @Description 修改系统 hosts +// @Success 200 +// @Security ApiKeyAuth +// @Router /toolbox/device/update/host [post] +// @x-panel-log {"bodyKeys":["key","value"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"修改主机 Host [key] => [value]","formatEN":"update device host [key] => [value]"} +func (b *BaseApi) UpdateDeviceHost(c *gin.Context) { + var req []dto.HostHelper + if err := helper.CheckBind(&req, c); err != nil { + return + } + + if err := deviceService.UpdateHosts(req); err != nil { + helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) + return + } + + helper.SuccessWithData(c, nil) +} + +// @Tags Device +// @Summary Update device passwd +// @Description 修改系统密码 +// @Accept json +// @Param request body dto.ChangePasswd true "request" +// @Success 200 +// @Security ApiKeyAuth +// @Router /toolbox/device/update/passwd [post] +func (b *BaseApi) UpdateDevicePasswd(c *gin.Context) { + var req dto.ChangePasswd + if err := helper.CheckBindAndValidate(&req, c); err != nil { + return + } + if len(req.Passwd) != 0 { + password, err := base64.StdEncoding.DecodeString(req.Passwd) + if err != nil { + helper.ErrorWithDetail(c, constant.CodeErrBadRequest, constant.ErrTypeInvalidParams, err) + return + } + req.Passwd = string(password) + } + if err := deviceService.UpdatePasswd(req); err != nil { + helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) + return + } + + helper.SuccessWithData(c, nil) +} + +// @Tags Device +// @Summary Update device swap +// @Description 修改系统 Swap +// @Accept json +// @Param request body dto.SwapHelper true "request" +// @Success 200 +// @Security ApiKeyAuth +// @Router /toolbox/device/update/swap [post] +// @x-panel-log {"bodyKeys":["operate","path"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"[operate] 主机 swap [path]","formatEN":"[operate] device swap [path]"} +func (b *BaseApi) UpdateDeviceSwap(c *gin.Context) { + var req dto.SwapHelper + if err := helper.CheckBindAndValidate(&req, c); err != nil { + return + } + if err := deviceService.UpdateSwap(req); err != nil { + helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) + return + } + + helper.SuccessWithData(c, nil) +} + +// @Tags Device +// @Summary Check device DNS conf +// @Description 检查系统 DNS 配置可用性 +// @Accept json +// @Param request body dto.SettingUpdate true "request" +// @Success 200 +// @Security ApiKeyAuth +// @Router /toolbox/device/check/dns [post] +func (b *BaseApi) CheckDNS(c *gin.Context) { + var req dto.SettingUpdate + if err := helper.CheckBindAndValidate(&req, c); err != nil { + return + } + + data, err := deviceService.CheckDNS(req.Key, req.Value) + if err != nil { + helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) + return + } + + helper.SuccessWithData(c, data) +} + +// @Tags Device +// @Summary Scan system +// @Description 扫描系统垃圾文件 +// @Success 200 +// @Security ApiKeyAuth +// @Router /toolbox/scan [post] +// @x-panel-log {"bodyKeys":[],"paramKeys":[],"BeforeFunctions":[],"formatZH":"扫描系统垃圾文件","formatEN":"scan System Junk Files"} +func (b *BaseApi) ScanSystem(c *gin.Context) { + helper.SuccessWithData(c, deviceService.Scan()) +} + +// @Tags Device +// @Summary Clean system +// @Description 清理系统垃圾文件 +// @Accept json +// @Param request body []dto.Clean true "request" +// @Success 200 +// @Security ApiKeyAuth +// @Router /toolbox/clean [post] +// @x-panel-log {"bodyKeys":[],"paramKeys":[],"BeforeFunctions":[],"formatZH":"清理系统垃圾文件","formatEN":"Clean system junk files"} +func (b *BaseApi) SystemClean(c *gin.Context) { + var req []dto.Clean + if err := helper.CheckBind(&req, c); err != nil { + return + } + + deviceService.Clean(req) + + helper.SuccessWithData(c, nil) +} diff --git a/agent/app/api/v1/docker.go b/agent/app/api/v1/docker.go new file mode 100644 index 000000000..1434521e4 --- /dev/null +++ b/agent/app/api/v1/docker.go @@ -0,0 +1,169 @@ +package v1 + +import ( + "os" + + "github.com/1Panel-dev/1Panel/agent/app/api/v1/helper" + "github.com/1Panel-dev/1Panel/agent/app/dto" + "github.com/1Panel-dev/1Panel/agent/constant" + "github.com/gin-gonic/gin" +) + +// @Tags Container Docker +// @Summary Load docker status +// @Description 获取 docker 服务状态 +// @Produce json +// @Success 200 {string} status +// @Security ApiKeyAuth +// @Router /containers/docker/status [get] +func (b *BaseApi) LoadDockerStatus(c *gin.Context) { + status := dockerService.LoadDockerStatus() + helper.SuccessWithData(c, status) +} + +// @Tags Container Docker +// @Summary Load docker daemon.json +// @Description 获取 docker 配置信息(表单) +// @Produce json +// @Success 200 {object} string +// @Security ApiKeyAuth +// @Router /containers/daemonjson/file [get] +func (b *BaseApi) LoadDaemonJsonFile(c *gin.Context) { + if _, err := os.Stat(constant.DaemonJsonPath); err != nil { + helper.SuccessWithData(c, "daemon.json is not find in path") + return + } + content, err := os.ReadFile(constant.DaemonJsonPath) + if err != nil { + helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) + return + } + helper.SuccessWithData(c, string(content)) +} + +// @Tags Container Docker +// @Summary Load docker daemon.json +// @Description 获取 docker 配置信息 +// @Produce json +// @Success 200 {object} dto.DaemonJsonConf +// @Security ApiKeyAuth +// @Router /containers/daemonjson [get] +func (b *BaseApi) LoadDaemonJson(c *gin.Context) { + conf := dockerService.LoadDockerConf() + helper.SuccessWithData(c, conf) +} + +// @Tags Container Docker +// @Summary Update docker daemon.json +// @Description 修改 docker 配置信息 +// @Accept json +// @Param request body dto.SettingUpdate true "request" +// @Success 200 +// @Security ApiKeyAuth +// @Router /containers/daemonjson/update [post] +// @x-panel-log {"bodyKeys":["key", "value"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"更新配置 [key]","formatEN":"Updated configuration [key]"} +func (b *BaseApi) UpdateDaemonJson(c *gin.Context) { + var req dto.SettingUpdate + if err := helper.CheckBindAndValidate(&req, c); err != nil { + return + } + + if err := dockerService.UpdateConf(req); err != nil { + helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) + return + } + + helper.SuccessWithData(c, nil) +} + +// @Tags Container Docker +// @Summary Update docker daemon.json log option +// @Description 修改 docker 日志配置 +// @Accept json +// @Param request body dto.LogOption true "request" +// @Success 200 +// @Security ApiKeyAuth +// @Router /containers/logoption/update [post] +// @x-panel-log {"bodyKeys":[],"paramKeys":[],"BeforeFunctions":[],"formatZH":"更新日志配置","formatEN":"Updated the log option"} +func (b *BaseApi) UpdateLogOption(c *gin.Context) { + var req dto.LogOption + if err := helper.CheckBind(&req, c); err != nil { + return + } + + if err := dockerService.UpdateLogOption(req); err != nil { + helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) + return + } + + helper.SuccessWithData(c, nil) +} + +// @Tags Container Docker +// @Summary Update docker daemon.json ipv6 option +// @Description 修改 docker ipv6 配置 +// @Accept json +// @Param request body dto.LogOption true "request" +// @Success 200 +// @Security ApiKeyAuth +// @Router /containers/ipv6option/update [post] +// @x-panel-log {"bodyKeys":[],"paramKeys":[],"BeforeFunctions":[],"formatZH":"更新 ipv6 配置","formatEN":"Updated the ipv6 option"} +func (b *BaseApi) UpdateIpv6Option(c *gin.Context) { + var req dto.Ipv6Option + if err := helper.CheckBind(&req, c); err != nil { + return + } + + if err := dockerService.UpdateIpv6Option(req); err != nil { + helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) + return + } + + helper.SuccessWithData(c, nil) +} + +// @Tags Container Docker +// @Summary Update docker daemon.json by upload file +// @Description 上传替换 docker 配置文件 +// @Accept json +// @Param request body dto.DaemonJsonUpdateByFile true "request" +// @Success 200 +// @Security ApiKeyAuth +// @Router /containers/daemonjson/update/byfile [post] +// @x-panel-log {"bodyKeys":[],"paramKeys":[],"BeforeFunctions":[],"formatZH":"更新配置文件","formatEN":"Updated configuration file"} +func (b *BaseApi) UpdateDaemonJsonByFile(c *gin.Context) { + var req dto.DaemonJsonUpdateByFile + if err := helper.CheckBindAndValidate(&req, c); err != nil { + return + } + + if err := dockerService.UpdateConfByFile(req); err != nil { + helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) + return + } + + helper.SuccessWithData(c, nil) +} + +// @Tags Container Docker +// @Summary Operate docker +// @Description Docker 操作 +// @Accept json +// @Param request body dto.DockerOperation true "request" +// @Success 200 +// @Security ApiKeyAuth +// @Router /containers/docker/operate [post] +// @x-panel-log {"bodyKeys":["operation"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"docker 服务 [operation]","formatEN":"[operation] docker service"} +func (b *BaseApi) OperateDocker(c *gin.Context) { + var req dto.DockerOperation + if err := helper.CheckBindAndValidate(&req, c); err != nil { + return + } + + if err := dockerService.OperateDocker(req); err != nil { + helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) + return + } + + helper.SuccessWithData(c, nil) +} diff --git a/agent/app/api/v1/entry.go b/agent/app/api/v1/entry.go new file mode 100644 index 000000000..d952afc47 --- /dev/null +++ b/agent/app/api/v1/entry.go @@ -0,0 +1,69 @@ +package v1 + +import "github.com/1Panel-dev/1Panel/agent/app/service" + +type ApiGroup struct { + BaseApi +} + +var ApiGroupApp = new(ApiGroup) + +type BaseApi struct{} + +var ( + dashboardService = service.NewIDashboardService() + + appService = service.NewIAppService() + appInstallService = service.NewIAppInstalledService() + + containerService = service.NewIContainerService() + composeTemplateService = service.NewIComposeTemplateService() + imageRepoService = service.NewIImageRepoService() + imageService = service.NewIImageService() + dockerService = service.NewIDockerService() + + dbCommonService = service.NewIDBCommonService() + mysqlService = service.NewIMysqlService() + postgresqlService = service.NewIPostgresqlService() + databaseService = service.NewIDatabaseService() + redisService = service.NewIRedisService() + + cronjobService = service.NewICronjobService() + + hostService = service.NewIHostService() + groupService = service.NewIGroupService() + fileService = service.NewIFileService() + sshService = service.NewISSHService() + firewallService = service.NewIFirewallService() + + deviceService = service.NewIDeviceService() + fail2banService = service.NewIFail2BanService() + ftpService = service.NewIFtpService() + clamService = service.NewIClamService() + + settingService = service.NewISettingService() + backupService = service.NewIBackupService() + + commandService = service.NewICommandService() + + websiteService = service.NewIWebsiteService() + websiteDnsAccountService = service.NewIWebsiteDnsAccountService() + websiteSSLService = service.NewIWebsiteSSLService() + websiteAcmeAccountService = service.NewIWebsiteAcmeAccountService() + + nginxService = service.NewINginxService() + + logService = service.NewILogService() + snapshotService = service.NewISnapshotService() + + runtimeService = service.NewRuntimeService() + processService = service.NewIProcessService() + phpExtensionsService = service.NewIPHPExtensionsService() + + hostToolService = service.NewIHostToolService() + + recycleBinService = service.NewIRecycleBinService() + favoriteService = service.NewIFavoriteService() + + websiteCAService = service.NewIWebsiteCAService() +) diff --git a/agent/app/api/v1/fail2ban.go b/agent/app/api/v1/fail2ban.go new file mode 100644 index 000000000..d0b18b320 --- /dev/null +++ b/agent/app/api/v1/fail2ban.go @@ -0,0 +1,153 @@ +package v1 + +import ( + "os" + + "github.com/1Panel-dev/1Panel/agent/app/api/v1/helper" + "github.com/1Panel-dev/1Panel/agent/app/dto" + "github.com/1Panel-dev/1Panel/agent/constant" + "github.com/gin-gonic/gin" +) + +// @Tags Fail2ban +// @Summary Load fail2ban base info +// @Description 获取 Fail2ban 基础信息 +// @Success 200 {object} dto.Fail2BanBaseInfo +// @Security ApiKeyAuth +// @Router /toolbox/fail2ban/base [get] +func (b *BaseApi) LoadFail2BanBaseInfo(c *gin.Context) { + data, err := fail2banService.LoadBaseInfo() + if err != nil { + helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) + return + } + + helper.SuccessWithData(c, data) +} + +// @Tags Fail2ban +// @Summary Page fail2ban ip list +// @Description 获取 Fail2ban ip +// @Accept json +// @Param request body dto.Fail2BanSearch true "request" +// @Success 200 {Array} string +// @Security ApiKeyAuth +// @Router /toolbox/fail2ban/search [post] +func (b *BaseApi) SearchFail2Ban(c *gin.Context) { + var req dto.Fail2BanSearch + if err := helper.CheckBindAndValidate(&req, c); err != nil { + return + } + + list, err := fail2banService.Search(req) + if err != nil { + helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) + return + } + + helper.SuccessWithData(c, list) +} + +// @Tags Fail2ban +// @Summary Operate fail2ban +// @Description 修改 Fail2ban 状态 +// @Accept json +// @Param request body dto.Operate true "request" +// @Security ApiKeyAuth +// @Router /toolbox/fail2ban/operate [post] +// @x-panel-log {"bodyKeys":["operation"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"[operation] Fail2ban","formatEN":"[operation] Fail2ban"} +func (b *BaseApi) OperateFail2Ban(c *gin.Context) { + var req dto.Operate + if err := helper.CheckBindAndValidate(&req, c); err != nil { + return + } + + if err := fail2banService.Operate(req.Operation); err != nil { + helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) + return + } + + helper.SuccessWithData(c, nil) +} + +// @Tags Fail2ban +// @Summary Operate sshd of fail2ban +// @Description 配置 sshd +// @Accept json +// @Param request body dto.Operate true "request" +// @Security ApiKeyAuth +// @Router /toolbox/fail2ban/operate/sshd [post] +func (b *BaseApi) OperateSSHD(c *gin.Context) { + var req dto.Fail2BanSet + if err := helper.CheckBindAndValidate(&req, c); err != nil { + return + } + + if err := fail2banService.OperateSSHD(req); err != nil { + helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) + return + } + + helper.SuccessWithData(c, nil) +} + +// @Tags Fail2ban +// @Summary Update fail2ban conf +// @Description 修改 Fail2ban 配置 +// @Accept json +// @Param request body dto.Fail2BanUpdate true "request" +// @Success 200 +// @Security ApiKeyAuth +// @Router /toolbox/fail2ban/update [post] +// @x-panel-log {"bodyKeys":["key","value"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"修改 Fail2ban 配置 [key] => [value]","formatEN":"update fail2ban conf [key] => [value]"} +func (b *BaseApi) UpdateFail2BanConf(c *gin.Context) { + var req dto.Fail2BanUpdate + if err := helper.CheckBindAndValidate(&req, c); err != nil { + return + } + + if err := fail2banService.UpdateConf(req); err != nil { + helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) + return + } + helper.SuccessWithData(c, nil) +} + +// @Tags Fail2ban +// @Summary Load fail2ban conf +// @Description 获取 fail2ban 配置文件 +// @Accept json +// @Success 200 +// @Security ApiKeyAuth +// @Router /toolbox/fail2ban/load/conf [get] +func (b *BaseApi) LoadFail2BanConf(c *gin.Context) { + path := "/etc/fail2ban/jail.local" + file, err := os.ReadFile(path) + if err != nil { + helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) + return + } + + helper.SuccessWithData(c, string(file)) +} + +// @Tags Fail2ban +// @Summary Update fail2ban conf by file +// @Description 通过文件修改 fail2ban 配置 +// @Accept json +// @Param request body dto.UpdateByFile true "request" +// @Success 200 +// @Security ApiKeyAuth +// @Router /toolbox/fail2ban/update/byconf [post] +func (b *BaseApi) UpdateFail2BanConfByFile(c *gin.Context) { + var req dto.UpdateByFile + if err := helper.CheckBind(&req, c); err != nil { + return + } + if err := fail2banService.UpdateConfByFile(req); err != nil { + helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) + return + } + + helper.SuccessWithData(c, nil) +} diff --git a/agent/app/api/v1/favorite.go b/agent/app/api/v1/favorite.go new file mode 100644 index 000000000..656fd0e8a --- /dev/null +++ b/agent/app/api/v1/favorite.go @@ -0,0 +1,76 @@ +package v1 + +import ( + "github.com/1Panel-dev/1Panel/agent/app/api/v1/helper" + "github.com/1Panel-dev/1Panel/agent/app/dto" + "github.com/1Panel-dev/1Panel/agent/app/dto/request" + "github.com/1Panel-dev/1Panel/agent/constant" + "github.com/gin-gonic/gin" +) + +// @Tags File +// @Summary List favorites +// @Description 获取收藏列表 +// @Accept json +// @Param request body dto.PageInfo true "request" +// @Success 200 +// @Security ApiKeyAuth +// @Router /files/favorite/search [post] +func (b *BaseApi) SearchFavorite(c *gin.Context) { + var req dto.PageInfo + if err := helper.CheckBindAndValidate(&req, c); err != nil { + return + } + total, list, err := favoriteService.Page(req) + if err != nil { + helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) + return + } + helper.SuccessWithData(c, dto.PageResult{ + Total: total, + Items: list, + }) +} + +// @Tags File +// @Summary Create favorite +// @Description 创建收藏 +// @Accept json +// @Param request body request.FavoriteCreate true "request" +// @Success 200 +// @Security ApiKeyAuth +// @Router /files/favorite [post] +// @x-panel-log {"bodyKeys":["path"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"收藏文件/文件夹 [path]","formatEN":"收藏文件/文件夹 [path]"} +func (b *BaseApi) CreateFavorite(c *gin.Context) { + var req request.FavoriteCreate + if err := helper.CheckBindAndValidate(&req, c); err != nil { + return + } + favorite, err := favoriteService.Create(req) + if err != nil { + helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) + return + } + helper.SuccessWithData(c, favorite) +} + +// @Tags File +// @Summary Delete favorite +// @Description 删除收藏 +// @Accept json +// @Param request body request.FavoriteDelete true "request" +// @Success 200 +// @Security ApiKeyAuth +// @Router /files/favorite/del [post] +// @x-panel-log {"bodyKeys":["id"],"paramKeys":[],"BeforeFunctions":[{"input_column":"id","input_value":"id","isList":false,"db":"favorites","output_column":"path","output_value":"path"}],"formatZH":"删除收藏 [path]","formatEN":"delete avorite [path]"} +func (b *BaseApi) DeleteFavorite(c *gin.Context) { + var req request.FavoriteDelete + if err := helper.CheckBindAndValidate(&req, c); err != nil { + return + } + if err := favoriteService.Delete(req.ID); err != nil { + helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) + return + } + helper.SuccessWithOutData(c) +} diff --git a/agent/app/api/v1/file.go b/agent/app/api/v1/file.go new file mode 100644 index 000000000..fa1be19c3 --- /dev/null +++ b/agent/app/api/v1/file.go @@ -0,0 +1,801 @@ +package v1 + +import ( + "errors" + "fmt" + "io" + "net/http" + "net/url" + "os" + "path" + "path/filepath" + "strconv" + "strings" + + "github.com/1Panel-dev/1Panel/agent/app/api/v1/helper" + "github.com/1Panel-dev/1Panel/agent/app/dto" + "github.com/1Panel-dev/1Panel/agent/app/dto/request" + "github.com/1Panel-dev/1Panel/agent/app/dto/response" + "github.com/1Panel-dev/1Panel/agent/buserr" + "github.com/1Panel-dev/1Panel/agent/constant" + "github.com/1Panel-dev/1Panel/agent/global" + "github.com/1Panel-dev/1Panel/agent/utils/files" + websocket2 "github.com/1Panel-dev/1Panel/agent/utils/websocket" + "github.com/gin-gonic/gin" + "github.com/gorilla/websocket" +) + +// @Tags File +// @Summary List files +// @Description 获取文件列表 +// @Accept json +// @Param request body request.FileOption true "request" +// @Success 200 {object} response.FileInfo +// @Security ApiKeyAuth +// @Router /files/search [post] +func (b *BaseApi) ListFiles(c *gin.Context) { + var req request.FileOption + if err := helper.CheckBindAndValidate(&req, c); err != nil { + return + } + files, err := fileService.GetFileList(req) + if err != nil { + helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) + return + } + helper.SuccessWithData(c, files) +} + +// @Tags File +// @Summary Page file +// @Description 分页获取上传文件 +// @Accept json +// @Param request body request.SearchUploadWithPage true "request" +// @Success 200 {array} response.FileInfo +// @Security ApiKeyAuth +// @Router /files/upload/search [post] +func (b *BaseApi) SearchUploadWithPage(c *gin.Context) { + var req request.SearchUploadWithPage + if err := helper.CheckBindAndValidate(&req, c); err != nil { + return + } + total, files, err := fileService.SearchUploadWithPage(req) + if err != nil { + helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) + return + } + helper.SuccessWithData(c, dto.PageResult{ + Items: files, + Total: total, + }) +} + +// @Tags File +// @Summary Load files tree +// @Description 加载文件树 +// @Accept json +// @Param request body request.FileOption true "request" +// @Success 200 {array} response.FileTree +// @Security ApiKeyAuth +// @Router /files/tree [post] +func (b *BaseApi) GetFileTree(c *gin.Context) { + var req request.FileOption + if err := helper.CheckBindAndValidate(&req, c); err != nil { + return + } + tree, err := fileService.GetFileTree(req) + if err != nil { + helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) + return + } + helper.SuccessWithData(c, tree) +} + +// @Tags File +// @Summary Create file +// @Description 创建文件/文件夹 +// @Accept json +// @Param request body request.FileCreate true "request" +// @Success 200 +// @Security ApiKeyAuth +// @Router /files [post] +// @x-panel-log {"bodyKeys":["path"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"创建文件/文件夹 [path]","formatEN":"Create dir or file [path]"} +func (b *BaseApi) CreateFile(c *gin.Context) { + var req request.FileCreate + if err := helper.CheckBindAndValidate(&req, c); err != nil { + return + } + err := fileService.Create(req) + if err != nil { + helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) + return + } + helper.SuccessWithData(c, nil) +} + +// @Tags File +// @Summary Delete file +// @Description 删除文件/文件夹 +// @Accept json +// @Param request body request.FileDelete true "request" +// @Success 200 +// @Security ApiKeyAuth +// @Router /files/del [post] +// @x-panel-log {"bodyKeys":["path"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"删除文件/文件夹 [path]","formatEN":"Delete dir or file [path]"} +func (b *BaseApi) DeleteFile(c *gin.Context) { + var req request.FileDelete + if err := helper.CheckBindAndValidate(&req, c); err != nil { + return + } + err := fileService.Delete(req) + if err != nil { + helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) + return + } + helper.SuccessWithData(c, nil) +} + +// @Tags File +// @Summary Batch delete file +// @Description 批量删除文件/文件夹 +// @Accept json +// @Param request body request.FileBatchDelete true "request" +// @Success 200 +// @Security ApiKeyAuth +// @Router /files/batch/del [post] +// @x-panel-log {"bodyKeys":["paths"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"批量删除文件/文件夹 [paths]","formatEN":"Batch delete dir or file [paths]"} +func (b *BaseApi) BatchDeleteFile(c *gin.Context) { + var req request.FileBatchDelete + if err := helper.CheckBindAndValidate(&req, c); err != nil { + return + } + err := fileService.BatchDelete(req) + if err != nil { + helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) + return + } + helper.SuccessWithData(c, nil) +} + +// @Tags File +// @Summary Change file mode +// @Description 修改文件权限 +// @Accept json +// @Param request body request.FileCreate true "request" +// @Success 200 +// @Security ApiKeyAuth +// @Router /files/mode [post] +// @x-panel-log {"bodyKeys":["path","mode"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"修改权限 [paths] => [mode]","formatEN":"Change mode [paths] => [mode]"} +func (b *BaseApi) ChangeFileMode(c *gin.Context) { + var req request.FileCreate + if err := helper.CheckBindAndValidate(&req, c); err != nil { + return + } + err := fileService.ChangeMode(req) + if err != nil { + helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) + return + } + helper.SuccessWithOutData(c) +} + +// @Tags File +// @Summary Change file owner +// @Description 修改文件用户/组 +// @Accept json +// @Param request body request.FileRoleUpdate true "request" +// @Success 200 +// @Security ApiKeyAuth +// @Router /files/owner [post] +// @x-panel-log {"bodyKeys":["path","user","group"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"修改用户/组 [paths] => [user]/[group]","formatEN":"Change owner [paths] => [user]/[group]"} +func (b *BaseApi) ChangeFileOwner(c *gin.Context) { + var req request.FileRoleUpdate + if err := helper.CheckBindAndValidate(&req, c); err != nil { + return + } + if err := fileService.ChangeOwner(req); err != nil { + helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) + return + } + helper.SuccessWithOutData(c) +} + +// @Tags File +// @Summary Compress file +// @Description 压缩文件 +// @Accept json +// @Param request body request.FileCompress true "request" +// @Success 200 +// @Security ApiKeyAuth +// @Router /files/compress [post] +// @x-panel-log {"bodyKeys":["name"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"压缩文件 [name]","formatEN":"Compress file [name]"} +func (b *BaseApi) CompressFile(c *gin.Context) { + var req request.FileCompress + if err := helper.CheckBindAndValidate(&req, c); err != nil { + return + } + err := fileService.Compress(req) + if err != nil { + helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) + return + } + helper.SuccessWithData(c, nil) +} + +// @Tags File +// @Summary Decompress file +// @Description 解压文件 +// @Accept json +// @Param request body request.FileDeCompress true "request" +// @Success 200 +// @Security ApiKeyAuth +// @Router /files/decompress [post] +// @x-panel-log {"bodyKeys":["path"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"解压 [path]","formatEN":"Decompress file [path]"} +func (b *BaseApi) DeCompressFile(c *gin.Context) { + var req request.FileDeCompress + if err := helper.CheckBindAndValidate(&req, c); err != nil { + return + } + err := fileService.DeCompress(req) + if err != nil { + helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) + return + } + helper.SuccessWithData(c, nil) +} + +// @Tags File +// @Summary Load file content +// @Description 获取文件内容 +// @Accept json +// @Param request body request.FileContentReq true "request" +// @Success 200 {object} response.FileInfo +// @Security ApiKeyAuth +// @Router /files/content [post] +// @x-panel-log {"bodyKeys":["path"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"获取文件内容 [path]","formatEN":"Load file content [path]"} +func (b *BaseApi) GetContent(c *gin.Context) { + var req request.FileContentReq + if err := helper.CheckBindAndValidate(&req, c); err != nil { + return + } + info, err := fileService.GetContent(req) + if err != nil { + helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) + return + } + helper.SuccessWithData(c, info) +} + +// @Tags File +// @Summary Update file content +// @Description 更新文件内容 +// @Accept json +// @Param request body request.FileEdit true "request" +// @Success 200 +// @Security ApiKeyAuth +// @Router /files/save [post] +// @x-panel-log {"bodyKeys":["path"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"更新文件内容 [path]","formatEN":"Update file content [path]"} +func (b *BaseApi) SaveContent(c *gin.Context) { + var req request.FileEdit + if err := helper.CheckBindAndValidate(&req, c); err != nil { + return + } + if err := fileService.SaveContent(req); err != nil { + helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) + return + } + helper.SuccessWithData(c, nil) +} + +// @Tags File +// @Summary Upload file +// @Description 上传文件 +// @Param file formData file true "request" +// @Success 200 +// @Security ApiKeyAuth +// @Router /files/upload [post] +// @x-panel-log {"bodyKeys":["path"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"上传文件 [path]","formatEN":"Upload file [path]"} +func (b *BaseApi) UploadFiles(c *gin.Context) { + form, err := c.MultipartForm() + if err != nil { + helper.ErrorWithDetail(c, constant.CodeErrBadRequest, constant.ErrTypeInvalidParams, err) + return + } + uploadFiles := form.File["file"] + paths := form.Value["path"] + + overwrite := true + if ow, ok := form.Value["overwrite"]; ok { + if len(ow) != 0 { + parseBool, _ := strconv.ParseBool(ow[0]) + overwrite = parseBool + } + } + + if len(paths) == 0 || !strings.Contains(paths[0], "/") { + helper.ErrorWithDetail(c, constant.CodeErrBadRequest, constant.ErrTypeInvalidParams, errors.New("error paths in request")) + return + } + dir := path.Dir(paths[0]) + + _, err = os.Stat(dir) + if err != nil && os.IsNotExist(err) { + mode, err := files.GetParentMode(dir) + if err != nil { + helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) + return + } + if err = os.MkdirAll(dir, mode); err != nil { + helper.ErrorWithDetail(c, constant.CodeErrBadRequest, constant.ErrTypeInvalidParams, fmt.Errorf("mkdir %s failed, err: %v", dir, err)) + return + } + } + info, err := os.Stat(dir) + if err != nil { + helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) + return + } + mode := info.Mode() + + fileOp := files.NewFileOp() + + success := 0 + failures := make(buserr.MultiErr) + for _, file := range uploadFiles { + dstFilename := path.Join(paths[0], file.Filename) + dstDir := path.Dir(dstFilename) + if !fileOp.Stat(dstDir) { + if err = fileOp.CreateDir(dstDir, mode); err != nil { + e := fmt.Errorf("create dir [%s] failed, err: %v", path.Dir(dstFilename), err) + failures[file.Filename] = e + global.LOG.Error(e) + continue + } + } + tmpFilename := dstFilename + ".tmp" + if err := c.SaveUploadedFile(file, tmpFilename); err != nil { + _ = os.Remove(tmpFilename) + e := fmt.Errorf("upload [%s] file failed, err: %v", file.Filename, err) + failures[file.Filename] = e + global.LOG.Error(e) + continue + } + dstInfo, statErr := os.Stat(dstFilename) + if overwrite { + _ = os.Remove(dstFilename) + } + + err = os.Rename(tmpFilename, dstFilename) + if err != nil { + _ = os.Remove(tmpFilename) + e := fmt.Errorf("upload [%s] file failed, err: %v", file.Filename, err) + failures[file.Filename] = e + global.LOG.Error(e) + continue + } + if statErr == nil { + _ = os.Chmod(dstFilename, dstInfo.Mode()) + } else { + _ = os.Chmod(dstFilename, mode) + } + success++ + } + if success == 0 { + helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, failures) + } else { + helper.SuccessWithMsg(c, fmt.Sprintf("%d files upload success", success)) + } +} + +// @Tags File +// @Summary Check file exist +// @Description 检测文件是否存在 +// @Accept json +// @Param request body request.FilePathCheck true "request" +// @Success 200 +// @Security ApiKeyAuth +// @Router /files/check [post] +func (b *BaseApi) CheckFile(c *gin.Context) { + var req request.FilePathCheck + if err := helper.CheckBindAndValidate(&req, c); err != nil { + return + } + if _, err := os.Stat(req.Path); err != nil { + helper.SuccessWithData(c, false) + return + } + helper.SuccessWithData(c, true) +} + +// @Tags File +// @Summary Change file name +// @Description 修改文件名称 +// @Accept json +// @Param request body request.FileRename true "request" +// @Success 200 +// @Security ApiKeyAuth +// @Router /files/rename [post] +// @x-panel-log {"bodyKeys":["oldName","newName"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"重命名 [oldName] => [newName]","formatEN":"Rename [oldName] => [newName]"} +func (b *BaseApi) ChangeFileName(c *gin.Context) { + var req request.FileRename + if err := helper.CheckBindAndValidate(&req, c); err != nil { + return + } + if err := fileService.ChangeName(req); err != nil { + helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) + return + } + helper.SuccessWithData(c, nil) +} + +// @Tags File +// @Summary Wget file +// @Description 下载远端文件 +// @Accept json +// @Param request body request.FileWget true "request" +// @Success 200 +// @Security ApiKeyAuth +// @Router /files/wget [post] +// @x-panel-log {"bodyKeys":["url","path","name"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"下载 url => [path]/[name]","formatEN":"Download url => [path]/[name]"} +func (b *BaseApi) WgetFile(c *gin.Context) { + var req request.FileWget + if err := helper.CheckBindAndValidate(&req, c); err != nil { + return + } + key, err := fileService.Wget(req) + if err != nil { + helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) + return + } + helper.SuccessWithData(c, response.FileWgetRes{ + Key: key, + }) +} + +// @Tags File +// @Summary Move file +// @Description 移动文件 +// @Accept json +// @Param request body request.FileMove true "request" +// @Success 200 +// @Security ApiKeyAuth +// @Router /files/move [post] +// @x-panel-log {"bodyKeys":["oldPaths","newPath"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"移动文件 [oldPaths] => [newPath]","formatEN":"Move [oldPaths] => [newPath]"} +func (b *BaseApi) MoveFile(c *gin.Context) { + var req request.FileMove + if err := helper.CheckBindAndValidate(&req, c); err != nil { + return + } + if err := fileService.MvFile(req); err != nil { + helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) + return + } + helper.SuccessWithData(c, nil) +} + +// @Tags File +// @Summary Download file +// @Description 下载文件 +// @Accept json +// @Success 200 +// @Security ApiKeyAuth +// @Router /files/download [get] +func (b *BaseApi) Download(c *gin.Context) { + filePath := c.Query("path") + file, err := os.Open(filePath) + if err != nil { + helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) + } + defer file.Close() + info, _ := file.Stat() + c.Header("Content-Length", strconv.FormatInt(info.Size(), 10)) + c.Header("Content-Disposition", "attachment; filename*=utf-8''"+url.PathEscape(info.Name())) + http.ServeContent(c.Writer, c.Request, info.Name(), info.ModTime(), file) +} + +// @Tags File +// @Summary Chunk Download file +// @Description 分片下载下载文件 +// @Accept json +// @Param request body request.FileDownload true "request" +// @Success 200 +// @Security ApiKeyAuth +// @Router /files/chunkdownload [post] +// @x-panel-log {"bodyKeys":["name"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"下载文件 [name]","formatEN":"Download file [name]"} +func (b *BaseApi) DownloadChunkFiles(c *gin.Context) { + var req request.FileChunkDownload + if err := helper.CheckBindAndValidate(&req, c); err != nil { + return + } + fileOp := files.NewFileOp() + if !fileOp.Stat(req.Path) { + helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrPathNotFound, nil) + return + } + filePath := req.Path + fstFile, err := fileOp.OpenFile(filePath) + if err != nil { + helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) + return + } + info, err := fstFile.Stat() + if err != nil { + helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) + return + } + if info.IsDir() { + helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrFileDownloadDir, err) + return + } + + c.Writer.Header().Set("Content-Disposition", fmt.Sprintf("attachment; filename=%s", req.Name)) + c.Writer.Header().Set("Content-Type", "application/octet-stream") + c.Writer.Header().Set("Content-Length", strconv.FormatInt(info.Size(), 10)) + c.Writer.Header().Set("Accept-Ranges", "bytes") + + if c.Request.Header.Get("Range") != "" { + rangeHeader := c.Request.Header.Get("Range") + rangeArr := strings.Split(rangeHeader, "=")[1] + rangeParts := strings.Split(rangeArr, "-") + + startPos, _ := strconv.ParseInt(rangeParts[0], 10, 64) + + var endPos int64 + if rangeParts[1] == "" { + endPos = info.Size() - 1 + } else { + endPos, _ = strconv.ParseInt(rangeParts[1], 10, 64) + } + + c.Writer.Header().Set("Content-Range", fmt.Sprintf("bytes %d-%d/%d", startPos, endPos, info.Size())) + c.Writer.WriteHeader(http.StatusPartialContent) + + buffer := make([]byte, 1024*1024) + file, err := os.Open(filePath) + if err != nil { + helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) + return + } + defer file.Close() + + _, _ = file.Seek(startPos, 0) + reader := io.LimitReader(file, endPos-startPos+1) + _, err = io.CopyBuffer(c.Writer, reader, buffer) + if err != nil { + helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) + return + } + } else { + c.File(filePath) + } +} + +// @Tags File +// @Summary Load file size +// @Description 获取文件夹大小 +// @Accept json +// @Param request body request.DirSizeReq true "request" +// @Success 200 +// @Security ApiKeyAuth +// @Router /files/size [post] +// @x-panel-log {"bodyKeys":["path"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"获取文件夹大小 [path]","formatEN":"Load file size [path]"} +func (b *BaseApi) Size(c *gin.Context) { + var req request.DirSizeReq + if err := helper.CheckBindAndValidate(&req, c); err != nil { + return + } + res, err := fileService.DirSize(req) + if err != nil { + helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) + return + } + helper.SuccessWithData(c, res) +} + +func mergeChunks(fileName string, fileDir string, dstDir string, chunkCount int, overwrite bool) error { + defer func() { + _ = os.RemoveAll(fileDir) + }() + + op := files.NewFileOp() + dstDir = strings.TrimSpace(dstDir) + mode, _ := files.GetParentMode(dstDir) + if mode == 0 { + mode = 0755 + } + if _, err := os.Stat(dstDir); err != nil && os.IsNotExist(err) { + if err = op.CreateDir(dstDir, mode); err != nil { + return err + } + } + dstFileName := filepath.Join(dstDir, fileName) + dstInfo, statErr := os.Stat(dstFileName) + if statErr == nil { + mode = dstInfo.Mode() + } else { + mode = 0644 + } + if overwrite { + _ = os.Remove(dstFileName) + } + targetFile, err := os.OpenFile(dstFileName, os.O_RDWR|os.O_CREATE, mode) + if err != nil { + return err + } + defer targetFile.Close() + for i := 0; i < chunkCount; i++ { + chunkPath := filepath.Join(fileDir, fmt.Sprintf("%s.%d", fileName, i)) + chunkData, err := os.ReadFile(chunkPath) + if err != nil { + return err + } + _, err = targetFile.Write(chunkData) + if err != nil { + return err + } + _ = os.Remove(chunkPath) + } + + return nil +} + +// @Tags File +// @Summary ChunkUpload file +// @Description 分片上传文件 +// @Param file formData file true "request" +// @Success 200 +// @Security ApiKeyAuth +// @Router /files/chunkupload [post] +func (b *BaseApi) UploadChunkFiles(c *gin.Context) { + var err error + fileForm, err := c.FormFile("chunk") + if err != nil { + helper.ErrorWithDetail(c, constant.CodeErrBadRequest, constant.ErrTypeInvalidParams, err) + return + } + uploadFile, err := fileForm.Open() + if err != nil { + helper.ErrorWithDetail(c, constant.CodeErrBadRequest, constant.ErrTypeInvalidParams, err) + return + } + defer uploadFile.Close() + chunkIndex, err := strconv.Atoi(c.PostForm("chunkIndex")) + if err != nil { + helper.ErrorWithDetail(c, constant.CodeErrBadRequest, constant.ErrTypeInvalidParams, err) + return + } + chunkCount, err := strconv.Atoi(c.PostForm("chunkCount")) + if err != nil { + helper.ErrorWithDetail(c, constant.CodeErrBadRequest, constant.ErrTypeInvalidParams, err) + return + } + fileOp := files.NewFileOp() + tmpDir := path.Join(global.CONF.System.TmpDir, "upload") + if !fileOp.Stat(tmpDir) { + if err := fileOp.CreateDir(tmpDir, 0755); err != nil { + helper.ErrorWithDetail(c, constant.CodeErrBadRequest, constant.ErrTypeInvalidParams, err) + return + } + } + filename := c.PostForm("filename") + fileDir := filepath.Join(tmpDir, filename) + if chunkIndex == 0 { + if fileOp.Stat(fileDir) { + _ = fileOp.DeleteDir(fileDir) + } + _ = os.MkdirAll(fileDir, 0755) + } + filePath := filepath.Join(fileDir, filename) + + defer func() { + if err != nil { + _ = os.Remove(fileDir) + } + }() + var ( + emptyFile *os.File + chunkData []byte + ) + + emptyFile, err = os.Create(filePath) + if err != nil { + helper.ErrorWithDetail(c, constant.CodeErrBadRequest, constant.ErrTypeInvalidParams, err) + return + } + defer emptyFile.Close() + + chunkData, err = io.ReadAll(uploadFile) + if err != nil { + helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, buserr.WithMap(constant.ErrFileUpload, map[string]interface{}{"name": filename, "detail": err.Error()}, err)) + return + } + + chunkPath := filepath.Join(fileDir, fmt.Sprintf("%s.%d", filename, chunkIndex)) + err = os.WriteFile(chunkPath, chunkData, 0644) + if err != nil { + helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, buserr.WithMap(constant.ErrFileUpload, map[string]interface{}{"name": filename, "detail": err.Error()}, err)) + return + } + + if chunkIndex+1 == chunkCount { + overwrite := true + if ow := c.PostForm("overwrite"); ow != "" { + overwrite, _ = strconv.ParseBool(ow) + } + err = mergeChunks(filename, fileDir, c.PostForm("path"), chunkCount, overwrite) + if err != nil { + helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, buserr.WithMap(constant.ErrFileUpload, map[string]interface{}{"name": filename, "detail": err.Error()}, err)) + return + } + helper.SuccessWithData(c, true) + } else { + return + } +} + +var wsUpgrade = websocket.Upgrader{ + CheckOrigin: func(r *http.Request) bool { + return true + }, +} + +func (b *BaseApi) Ws(c *gin.Context) { + ws, err := wsUpgrade.Upgrade(c.Writer, c.Request, nil) + if err != nil { + return + } + wsClient := websocket2.NewWsClient("fileClient", ws) + go wsClient.Read() + go wsClient.Write() +} + +func (b *BaseApi) Keys(c *gin.Context) { + res := &response.FileProcessKeys{} + keys, err := global.CACHE.PrefixScanKey("file-wget-") + if err != nil { + helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) + return + } + res.Keys = keys + helper.SuccessWithData(c, res) +} + +// @Tags File +// @Summary Read file by Line +// @Description 按行读取日志文件 +// @Param request body request.FileReadByLineReq true "request" +// @Success 200 +// @Security ApiKeyAuth +// @Router /files/read [post] +func (b *BaseApi) ReadFileByLine(c *gin.Context) { + var req request.FileReadByLineReq + if err := helper.CheckBindAndValidate(&req, c); err != nil { + return + } + res, err := fileService.ReadLogByLine(req) + if err != nil { + helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) + return + } + helper.SuccessWithData(c, res) +} + +// @Tags File +// @Summary Batch change file mode and owner +// @Description 批量修改文件权限和用户/组 +// @Accept json +// @Param request body request.FileRoleReq true "request" +// @Success 200 +// @Security ApiKeyAuth +// @Router /files/batch/role [post] +// @x-panel-log {"bodyKeys":["paths","mode","user","group"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"批量修改文件权限和用户/组 [paths] => [mode]/[user]/[group]","formatEN":"Batch change file mode and owner [paths] => [mode]/[user]/[group]"} +func (b *BaseApi) BatchChangeModeAndOwner(c *gin.Context) { + var req request.FileRoleReq + if err := helper.CheckBindAndValidate(&req, c); err != nil { + return + } + if err := fileService.BatchChangeModeAndOwner(req); err != nil { + helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) + } + helper.SuccessWithOutData(c) +} diff --git a/agent/app/api/v1/firewall.go b/agent/app/api/v1/firewall.go new file mode 100644 index 000000000..d2f68b711 --- /dev/null +++ b/agent/app/api/v1/firewall.go @@ -0,0 +1,224 @@ +package v1 + +import ( + "github.com/1Panel-dev/1Panel/agent/app/api/v1/helper" + "github.com/1Panel-dev/1Panel/agent/app/dto" + "github.com/1Panel-dev/1Panel/agent/constant" + "github.com/gin-gonic/gin" +) + +// @Tags Firewall +// @Summary Load firewall base info +// @Description 获取防火墙基础信息 +// @Success 200 {object} dto.FirewallBaseInfo +// @Security ApiKeyAuth +// @Router /hosts/firewall/base [get] +func (b *BaseApi) LoadFirewallBaseInfo(c *gin.Context) { + data, err := firewallService.LoadBaseInfo() + if err != nil { + helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) + return + } + + helper.SuccessWithData(c, data) +} + +// @Tags Firewall +// @Summary Page firewall rules +// @Description 获取防火墙规则列表分页 +// @Accept json +// @Param request body dto.RuleSearch true "request" +// @Success 200 {object} dto.PageResult +// @Security ApiKeyAuth +// @Router /hosts/firewall/search [post] +func (b *BaseApi) SearchFirewallRule(c *gin.Context) { + var req dto.RuleSearch + if err := helper.CheckBindAndValidate(&req, c); err != nil { + return + } + + total, list, err := firewallService.SearchWithPage(req) + if err != nil { + helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) + return + } + + helper.SuccessWithData(c, dto.PageResult{ + Items: list, + Total: total, + }) +} + +// @Tags Firewall +// @Summary Page firewall status +// @Description 修改防火墙状态 +// @Accept json +// @Param request body dto.FirewallOperation true "request" +// @Success 200 {object} dto.PageResult +// @Security ApiKeyAuth +// @Router /hosts/firewall/operate [post] +// @x-panel-log {"bodyKeys":["operation"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"[operation] 防火墙","formatEN":"[operation] firewall"} +func (b *BaseApi) OperateFirewall(c *gin.Context) { + var req dto.FirewallOperation + if err := helper.CheckBindAndValidate(&req, c); err != nil { + return + } + + if err := firewallService.OperateFirewall(req.Operation); err != nil { + helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) + return + } + + helper.SuccessWithData(c, nil) +} + +// @Tags Firewall +// @Summary Create group +// @Description 创建防火墙端口规则 +// @Accept json +// @Param request body dto.PortRuleOperate true "request" +// @Success 200 +// @Security ApiKeyAuth +// @Router /hosts/firewall/port [post] +// @x-panel-log {"bodyKeys":["port","strategy"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"添加端口规则 [strategy] [port]","formatEN":"create port rules [strategy][port]"} +func (b *BaseApi) OperatePortRule(c *gin.Context) { + var req dto.PortRuleOperate + if err := helper.CheckBindAndValidate(&req, c); err != nil { + return + } + + if err := firewallService.OperatePortRule(req, true); err != nil { + helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) + return + } + helper.SuccessWithData(c, nil) +} + +// OperateForwardRule +// @Tags Firewall +// @Summary Create group +// @Description 更新防火墙端口转发规则 +// @Accept json +// @Param request body dto.ForwardRuleOperate true "request" +// @Success 200 +// @Security ApiKeyAuth +// @Router /hosts/firewall/forward [post] +// @x-panel-log {"bodyKeys":["source_port"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"更新端口转发规则 [source_port]","formatEN":"update port forward rules [source_port]"} +func (b *BaseApi) OperateForwardRule(c *gin.Context) { + var req dto.ForwardRuleOperate + if err := helper.CheckBindAndValidate(&req, c); err != nil { + return + } + + if err := firewallService.OperateForwardRule(req); err != nil { + helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) + return + } + helper.SuccessWithData(c, nil) +} + +// @Tags Firewall +// @Summary Create group +// @Description 创建防火墙 IP 规则 +// @Accept json +// @Param request body dto.AddrRuleOperate true "request" +// @Success 200 +// @Security ApiKeyAuth +// @Router /hosts/firewall/ip [post] +// @x-panel-log {"bodyKeys":["strategy","address"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"添加 ip 规则 [strategy] [address]","formatEN":"create address rules [strategy][address]"} +func (b *BaseApi) OperateIPRule(c *gin.Context) { + var req dto.AddrRuleOperate + if err := helper.CheckBindAndValidate(&req, c); err != nil { + return + } + + if err := firewallService.OperateAddressRule(req, true); err != nil { + helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) + return + } + helper.SuccessWithData(c, nil) +} + +// @Tags Firewall +// @Summary Create group +// @Description 批量删除防火墙规则 +// @Accept json +// @Param request body dto.BatchRuleOperate true "request" +// @Success 200 +// @Security ApiKeyAuth +// @Router /hosts/firewall/batch [post] +func (b *BaseApi) BatchOperateRule(c *gin.Context) { + var req dto.BatchRuleOperate + if err := helper.CheckBindAndValidate(&req, c); err != nil { + return + } + + if err := firewallService.BatchOperateRule(req); err != nil { + helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) + return + } + helper.SuccessWithData(c, nil) +} + +// @Tags Firewall +// @Summary Update rule description +// @Description 更新防火墙描述 +// @Accept json +// @Param request body dto.UpdateFirewallDescription true "request" +// @Success 200 +// @Security ApiKeyAuth +// @Router /hosts/firewall/update/description [post] +func (b *BaseApi) UpdateFirewallDescription(c *gin.Context) { + var req dto.UpdateFirewallDescription + if err := helper.CheckBindAndValidate(&req, c); err != nil { + return + } + + if err := firewallService.UpdateDescription(req); err != nil { + helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) + return + } + helper.SuccessWithData(c, nil) +} + +// @Tags Firewall +// @Summary Create group +// @Description 更新端口防火墙规则 +// @Accept json +// @Param request body dto.PortRuleUpdate true "request" +// @Success 200 +// @Security ApiKeyAuth +// @Router /hosts/firewall/update/port [post] +func (b *BaseApi) UpdatePortRule(c *gin.Context) { + var req dto.PortRuleUpdate + if err := helper.CheckBindAndValidate(&req, c); err != nil { + return + } + + if err := firewallService.UpdatePortRule(req); err != nil { + helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) + return + } + helper.SuccessWithData(c, nil) +} + +// @Tags Firewall +// @Summary Create group +// @Description 更新 ip 防火墙规则 +// @Accept json +// @Param request body dto.AddrRuleUpdate true "request" +// @Success 200 +// @Security ApiKeyAuth +// @Router /hosts/firewall/update/addr [post] +func (b *BaseApi) UpdateAddrRule(c *gin.Context) { + var req dto.AddrRuleUpdate + if err := helper.CheckBindAndValidate(&req, c); err != nil { + return + } + + if err := firewallService.UpdateAddrRule(req); err != nil { + helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) + return + } + helper.SuccessWithData(c, nil) +} diff --git a/agent/app/api/v1/ftp.go b/agent/app/api/v1/ftp.go new file mode 100644 index 000000000..e95423acb --- /dev/null +++ b/agent/app/api/v1/ftp.go @@ -0,0 +1,199 @@ +package v1 + +import ( + "encoding/base64" + + "github.com/1Panel-dev/1Panel/agent/app/api/v1/helper" + "github.com/1Panel-dev/1Panel/agent/app/dto" + "github.com/1Panel-dev/1Panel/agent/constant" + "github.com/gin-gonic/gin" +) + +// @Tags FTP +// @Summary Load FTP base info +// @Description 获取 FTP 基础信息 +// @Success 200 {object} dto.FtpBaseInfo +// @Security ApiKeyAuth +// @Router /toolbox/ftp/base [get] +func (b *BaseApi) LoadFtpBaseInfo(c *gin.Context) { + data, err := ftpService.LoadBaseInfo() + if err != nil { + helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) + return + } + + helper.SuccessWithData(c, data) +} + +// @Tags FTP +// @Summary Load FTP operation log +// @Description 获取 FTP 操作日志 +// @Accept json +// @Param request body dto.FtpLogSearch true "request" +// @Success 200 {object} dto.PageResult +// @Security ApiKeyAuth +// @Router /toolbox/ftp/log/search [post] +func (b *BaseApi) LoadFtpLogInfo(c *gin.Context) { + var req dto.FtpLogSearch + if err := helper.CheckBindAndValidate(&req, c); err != nil { + return + } + + total, list, err := ftpService.LoadLog(req) + if err != nil { + helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) + return + } + + helper.SuccessWithData(c, dto.PageResult{ + Items: list, + Total: total, + }) +} + +// @Tags FTP +// @Summary Operate FTP +// @Description 修改 FTP 状态 +// @Accept json +// @Param request body dto.Operate true "request" +// @Security ApiKeyAuth +// @Router /toolbox/ftp/operate [post] +// @x-panel-log {"bodyKeys":["operation"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"[operation] FTP","formatEN":"[operation] FTP"} +func (b *BaseApi) OperateFtp(c *gin.Context) { + var req dto.Operate + if err := helper.CheckBindAndValidate(&req, c); err != nil { + return + } + + if err := ftpService.Operate(req.Operation); err != nil { + helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) + return + } + + helper.SuccessWithData(c, nil) +} + +// @Tags FTP +// @Summary Page FTP user +// @Description 获取 FTP 账户列表分页 +// @Accept json +// @Param request body dto.SearchWithPage true "request" +// @Success 200 {object} dto.PageResult +// @Security ApiKeyAuth +// @Router /toolbox/ftp/search [post] +func (b *BaseApi) SearchFtp(c *gin.Context) { + var req dto.SearchWithPage + if err := helper.CheckBindAndValidate(&req, c); err != nil { + return + } + + total, list, err := ftpService.SearchWithPage(req) + if err != nil { + helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) + return + } + + helper.SuccessWithData(c, dto.PageResult{ + Items: list, + Total: total, + }) +} + +// @Tags FTP +// @Summary Create FTP user +// @Description 创建 FTP 账户 +// @Accept json +// @Param request body dto.FtpCreate true "request" +// @Success 200 +// @Security ApiKeyAuth +// @Router /toolbox/ftp [post] +// @x-panel-log {"bodyKeys":["user", "path"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"创建 FTP 账户 [user][path]","formatEN":"create FTP [user][path]"} +func (b *BaseApi) CreateFtp(c *gin.Context) { + var req dto.FtpCreate + if err := helper.CheckBindAndValidate(&req, c); err != nil { + return + } + + if len(req.Password) != 0 { + pass, err := base64.StdEncoding.DecodeString(req.Password) + if err != nil { + helper.ErrorWithDetail(c, constant.CodeErrBadRequest, constant.ErrTypeInvalidParams, err) + return + } + req.Password = string(pass) + } + if _, err := ftpService.Create(req); err != nil { + helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) + return + } + helper.SuccessWithOutData(c) +} + +// @Tags FTP +// @Summary Delete FTP user +// @Description 删除 FTP 账户 +// @Accept json +// @Param request body dto.BatchDeleteReq true "request" +// @Success 200 +// @Security ApiKeyAuth +// @Router /toolbox/ftp/del [post] +// @x-panel-log {"bodyKeys":["ids"],"paramKeys":[],"BeforeFunctions":[{"input_column":"id","input_value":"ids","isList":true,"db":"ftps","output_column":"user","output_value":"users"}],"formatZH":"删除 FTP 账户 [users]","formatEN":"delete FTP users [users]"} +func (b *BaseApi) DeleteFtp(c *gin.Context) { + var req dto.BatchDeleteReq + if err := helper.CheckBindAndValidate(&req, c); err != nil { + return + } + + if err := ftpService.Delete(req); err != nil { + helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) + return + } + helper.SuccessWithOutData(c) +} + +// @Tags FTP +// @Summary Sync FTP user +// @Description 同步 FTP 账户 +// @Accept json +// @Param request body dto.BatchDeleteReq true "request" +// @Success 200 +// @Security ApiKeyAuth +// @Router /toolbox/ftp/sync [post] +// @x-panel-log {"bodyKeys":[],"paramKeys":[],"BeforeFunctions":[],"formatZH":"同步 FTP 账户","formatEN":"sync FTP users"} +func (b *BaseApi) SyncFtp(c *gin.Context) { + if err := ftpService.Sync(); err != nil { + helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) + return + } + helper.SuccessWithOutData(c) +} + +// @Tags FTP +// @Summary Update FTP user +// @Description 修改 FTP 账户 +// @Accept json +// @Param request body dto.FtpUpdate true "request" +// @Success 200 +// @Security ApiKeyAuth +// @Router /toolbox/ftp/update [post] +// @x-panel-log {"bodyKeys":["user", "path"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"修改 FTP 账户 [user][path]","formatEN":"update FTP [user][path]"} +func (b *BaseApi) UpdateFtp(c *gin.Context) { + var req dto.FtpUpdate + if err := helper.CheckBindAndValidate(&req, c); err != nil { + return + } + + if len(req.Password) != 0 { + pass, err := base64.StdEncoding.DecodeString(req.Password) + if err != nil { + helper.ErrorWithDetail(c, constant.CodeErrBadRequest, constant.ErrTypeInvalidParams, err) + return + } + req.Password = string(pass) + } + if err := ftpService.Update(req); err != nil { + helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) + return + } + helper.SuccessWithOutData(c) +} diff --git a/agent/app/api/v1/group.go b/agent/app/api/v1/group.go new file mode 100644 index 000000000..6a93cb451 --- /dev/null +++ b/agent/app/api/v1/group.go @@ -0,0 +1,97 @@ +package v1 + +import ( + "github.com/1Panel-dev/1Panel/agent/app/api/v1/helper" + "github.com/1Panel-dev/1Panel/agent/app/dto" + "github.com/1Panel-dev/1Panel/agent/constant" + "github.com/gin-gonic/gin" +) + +// @Tags System Group +// @Summary Create group +// @Description 创建系统组 +// @Accept json +// @Param request body dto.GroupCreate true "request" +// @Success 200 +// @Security ApiKeyAuth +// @Router /groups [post] +// @x-panel-log {"bodyKeys":["name","type"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"创建组 [name][type]","formatEN":"create group [name][type]"} +func (b *BaseApi) CreateGroup(c *gin.Context) { + var req dto.GroupCreate + if err := helper.CheckBindAndValidate(&req, c); err != nil { + return + } + + if err := groupService.Create(req); err != nil { + helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) + return + } + helper.SuccessWithData(c, nil) +} + +// @Tags System Group +// @Summary Delete group +// @Description 删除系统组 +// @Accept json +// @Param request body dto.OperateByID true "request" +// @Success 200 +// @Security ApiKeyAuth +// @Router /groups/del [post] +// @x-panel-log {"bodyKeys":["id"],"paramKeys":[],"BeforeFunctions":[{"input_column":"id","input_value":"id","isList":false,"db":"groups","output_column":"name","output_value":"name"},{"input_column":"id","input_value":"id","isList":false,"db":"groups","output_column":"type","output_value":"type"}],"formatZH":"删除组 [type][name]","formatEN":"delete group [type][name]"} +func (b *BaseApi) DeleteGroup(c *gin.Context) { + var req dto.OperateByID + if err := helper.CheckBindAndValidate(&req, c); err != nil { + return + } + + if err := groupService.Delete(req.ID); err != nil { + helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) + return + } + helper.SuccessWithData(c, nil) +} + +// @Tags System Group +// @Summary Update group +// @Description 更新系统组 +// @Accept json +// @Param request body dto.GroupUpdate true "request" +// @Success 200 +// @Security ApiKeyAuth +// @Router /groups/update [post] +// @x-panel-log {"bodyKeys":["name","type"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"更新组 [name][type]","formatEN":"update group [name][type]"} +func (b *BaseApi) UpdateGroup(c *gin.Context) { + var req dto.GroupUpdate + if err := helper.CheckBindAndValidate(&req, c); err != nil { + return + } + + if err := groupService.Update(req); err != nil { + helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) + return + } + helper.SuccessWithData(c, nil) +} + +// @Tags System Group +// @Summary List groups +// @Description 查询系统组 +// @Accept json +// @Param request body dto.GroupSearch true "request" +// @Success 200 {array} dto.GroupInfo +// @Security ApiKeyAuth +// @Router /groups/search [post] +func (b *BaseApi) ListGroup(c *gin.Context) { + var req dto.GroupSearch + if err := helper.CheckBindAndValidate(&req, c); err != nil { + return + } + + list, err := groupService.List(req) + if err != nil { + helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) + return + } + + helper.SuccessWithData(c, list) +} diff --git a/agent/app/api/v1/helper/helper.go b/agent/app/api/v1/helper/helper.go new file mode 100644 index 000000000..36142c862 --- /dev/null +++ b/agent/app/api/v1/helper/helper.go @@ -0,0 +1,140 @@ +package helper + +import ( + "context" + "fmt" + "net/http" + "strconv" + + "github.com/1Panel-dev/1Panel/agent/global" + "gorm.io/gorm" + + "github.com/1Panel-dev/1Panel/agent/app/dto" + "github.com/1Panel-dev/1Panel/agent/buserr" + "github.com/1Panel-dev/1Panel/agent/constant" + "github.com/1Panel-dev/1Panel/agent/i18n" + "github.com/gin-gonic/gin" + "github.com/pkg/errors" +) + +func ErrorWithDetail(ctx *gin.Context, code int, msgKey string, err error) { + res := dto.Response{ + Code: code, + Message: "", + } + if msgKey == constant.ErrTypeInternalServer { + switch { + case errors.Is(err, constant.ErrRecordExist): + res.Message = i18n.GetMsgWithMap("ErrRecordExist", nil) + case errors.Is(constant.ErrRecordNotFound, err): + res.Message = i18n.GetMsgWithMap("ErrRecordNotFound", nil) + case errors.Is(constant.ErrInvalidParams, err): + res.Message = i18n.GetMsgWithMap("ErrInvalidParams", nil) + case errors.Is(constant.ErrStructTransform, err): + res.Message = i18n.GetMsgWithMap("ErrStructTransform", map[string]interface{}{"detail": err}) + case errors.Is(constant.ErrCaptchaCode, err): + res.Code = constant.CodeAuth + res.Message = "ErrCaptchaCode" + case errors.Is(constant.ErrAuth, err): + res.Code = constant.CodeAuth + res.Message = "ErrAuth" + case errors.Is(constant.ErrInitialPassword, err): + res.Message = i18n.GetMsgWithMap("ErrInitialPassword", map[string]interface{}{"detail": err}) + case errors.As(err, &buserr.BusinessError{}): + res.Message = err.Error() + default: + res.Message = i18n.GetMsgWithMap(msgKey, map[string]interface{}{"detail": err}) + } + } else { + res.Message = i18n.GetMsgWithMap(msgKey, map[string]interface{}{"detail": err}) + } + ctx.JSON(http.StatusOK, res) + ctx.Abort() +} + +func SuccessWithData(ctx *gin.Context, data interface{}) { + if data == nil { + data = gin.H{} + } + res := dto.Response{ + Code: constant.CodeSuccess, + Data: data, + } + ctx.JSON(http.StatusOK, res) + ctx.Abort() +} + +func SuccessWithOutData(ctx *gin.Context) { + res := dto.Response{ + Code: constant.CodeSuccess, + Message: "success", + } + ctx.JSON(http.StatusOK, res) + ctx.Abort() +} + +func SuccessWithMsg(ctx *gin.Context, msg string) { + res := dto.Response{ + Code: constant.CodeSuccess, + Message: msg, + } + ctx.JSON(http.StatusOK, res) + ctx.Abort() +} + +func GetParamID(c *gin.Context) (uint, error) { + idParam, ok := c.Params.Get("id") + if !ok { + return 0, errors.New("error id in path") + } + intNum, _ := strconv.Atoi(idParam) + return uint(intNum), nil +} + +func GetIntParamByKey(c *gin.Context, key string) (uint, error) { + idParam, ok := c.Params.Get(key) + if !ok { + return 0, fmt.Errorf("error %s in path", key) + } + intNum, _ := strconv.Atoi(idParam) + return uint(intNum), nil +} + +func GetStrParamByKey(c *gin.Context, key string) (string, error) { + idParam, ok := c.Params.Get(key) + if !ok { + return "", fmt.Errorf("error %s in path", key) + } + return idParam, nil +} + +func GetTxAndContext() (tx *gorm.DB, ctx context.Context) { + tx = global.DB.Begin() + ctx = context.WithValue(context.Background(), constant.DB, tx) + return +} + +func CheckBindAndValidate(req interface{}, c *gin.Context) error { + if err := c.ShouldBindJSON(req); err != nil { + ErrorWithDetail(c, constant.CodeErrBadRequest, constant.ErrTypeInvalidParams, err) + return err + } + if err := global.VALID.Struct(req); err != nil { + ErrorWithDetail(c, constant.CodeErrBadRequest, constant.ErrTypeInvalidParams, err) + return err + } + return nil +} + +func CheckBind(req interface{}, c *gin.Context) error { + if err := c.ShouldBindJSON(&req); err != nil { + ErrorWithDetail(c, constant.CodeErrBadRequest, constant.ErrTypeInvalidParams, err) + return err + } + return nil +} + +func ErrResponse(ctx *gin.Context, code int) { + ctx.JSON(code, nil) + ctx.Abort() +} diff --git a/agent/app/api/v1/host.go b/agent/app/api/v1/host.go new file mode 100644 index 000000000..43ff9ec17 --- /dev/null +++ b/agent/app/api/v1/host.go @@ -0,0 +1,230 @@ +package v1 + +import ( + "github.com/1Panel-dev/1Panel/agent/app/api/v1/helper" + "github.com/1Panel-dev/1Panel/agent/app/dto" + "github.com/1Panel-dev/1Panel/agent/constant" + "github.com/1Panel-dev/1Panel/agent/utils/encrypt" + "github.com/gin-gonic/gin" +) + +// @Tags Host +// @Summary Create host +// @Description 创建主机 +// @Accept json +// @Param request body dto.HostOperate true "request" +// @Success 200 +// @Security ApiKeyAuth +// @Router /hosts [post] +// @x-panel-log {"bodyKeys":["name","addr"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"创建主机 [name][addr]","formatEN":"create host [name][addr]"} +func (b *BaseApi) CreateHost(c *gin.Context) { + var req dto.HostOperate + if err := helper.CheckBindAndValidate(&req, c); err != nil { + return + } + + host, err := hostService.Create(req) + if err != nil { + helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) + return + } + helper.SuccessWithData(c, host) +} + +// @Tags Host +// @Summary Test host conn by info +// @Description 测试主机连接 +// @Accept json +// @Param request body dto.HostConnTest true "request" +// @Success 200 +// @Security ApiKeyAuth +// @Router /hosts/test/byinfo [post] +func (b *BaseApi) TestByInfo(c *gin.Context) { + var req dto.HostConnTest + if err := helper.CheckBindAndValidate(&req, c); err != nil { + return + } + + connStatus := hostService.TestByInfo(req) + helper.SuccessWithData(c, connStatus) +} + +// @Tags Host +// @Summary Test host conn by host id +// @Description 测试主机连接 +// @Accept json +// @Param id path integer true "request" +// @Success 200 {boolean} connStatus +// @Security ApiKeyAuth +// @Router /hosts/test/byid/:id [post] +func (b *BaseApi) TestByID(c *gin.Context) { + id, err := helper.GetParamID(c) + if err != nil { + helper.ErrorWithDetail(c, constant.CodeErrBadRequest, constant.ErrTypeInvalidParams, err) + return + } + + connStatus := hostService.TestLocalConn(id) + helper.SuccessWithData(c, connStatus) +} + +// @Tags Host +// @Summary Load host tree +// @Description 加载主机树 +// @Accept json +// @Param request body dto.SearchForTree true "request" +// @Success 200 {array} dto.HostTree +// @Security ApiKeyAuth +// @Router /hosts/tree [post] +func (b *BaseApi) HostTree(c *gin.Context) { + var req dto.SearchForTree + if err := helper.CheckBind(&req, c); err != nil { + return + } + + data, err := hostService.SearchForTree(req) + if err != nil { + helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) + return + } + + helper.SuccessWithData(c, data) +} + +// @Tags Host +// @Summary Page host +// @Description 获取主机列表分页 +// @Accept json +// @Param request body dto.SearchHostWithPage true "request" +// @Success 200 {array} dto.HostTree +// @Security ApiKeyAuth +// @Router /hosts/search [post] +func (b *BaseApi) SearchHost(c *gin.Context) { + var req dto.SearchHostWithPage + if err := helper.CheckBindAndValidate(&req, c); err != nil { + return + } + + total, list, err := hostService.SearchWithPage(req) + if err != nil { + helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) + return + } + + helper.SuccessWithData(c, dto.PageResult{ + Items: list, + Total: total, + }) +} + +// @Tags Host +// @Summary Delete host +// @Description 删除主机 +// @Accept json +// @Param request body dto.BatchDeleteReq true "request" +// @Success 200 +// @Security ApiKeyAuth +// @Router /hosts/del [post] +// @x-panel-log {"bodyKeys":["ids"],"paramKeys":[],"BeforeFunctions":[{"input_column":"id","input_value":"ids","isList":true,"db":"hosts","output_column":"addr","output_value":"addrs"}],"formatZH":"删除主机 [addrs]","formatEN":"delete host [addrs]"} +func (b *BaseApi) DeleteHost(c *gin.Context) { + var req dto.BatchDeleteReq + if err := helper.CheckBindAndValidate(&req, c); err != nil { + return + } + + if err := hostService.Delete(req.Ids); err != nil { + helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) + return + } + helper.SuccessWithData(c, nil) +} + +// @Tags Host +// @Summary Update host +// @Description 更新主机 +// @Accept json +// @Param request body dto.HostOperate true "request" +// @Success 200 +// @Security ApiKeyAuth +// @Router /hosts/update [post] +// @x-panel-log {"bodyKeys":["name","addr"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"更新主机信息 [name][addr]","formatEN":"update host [name][addr]"} +func (b *BaseApi) UpdateHost(c *gin.Context) { + var req dto.HostOperate + if err := helper.CheckBindAndValidate(&req, c); err != nil { + return + } + + var err error + if len(req.Password) != 0 && req.AuthMode == "password" { + req.Password, err = hostService.EncryptHost(req.Password) + if err != nil { + helper.ErrorWithDetail(c, constant.CodeErrBadRequest, constant.ErrTypeInvalidParams, err) + return + } + req.PrivateKey = "" + req.PassPhrase = "" + } + if len(req.PrivateKey) != 0 && req.AuthMode == "key" { + req.PrivateKey, err = hostService.EncryptHost(req.PrivateKey) + if err != nil { + helper.ErrorWithDetail(c, constant.CodeErrBadRequest, constant.ErrTypeInvalidParams, err) + return + } + if len(req.PassPhrase) != 0 { + req.PassPhrase, err = encrypt.StringEncrypt(req.PassPhrase) + if err != nil { + helper.ErrorWithDetail(c, constant.CodeErrBadRequest, constant.ErrTypeInvalidParams, err) + return + } + } + req.Password = "" + } + + upMap := make(map[string]interface{}) + upMap["name"] = req.Name + upMap["group_id"] = req.GroupID + upMap["addr"] = req.Addr + upMap["port"] = req.Port + upMap["user"] = req.User + upMap["auth_mode"] = req.AuthMode + upMap["remember_password"] = req.RememberPassword + if req.AuthMode == "password" { + upMap["password"] = req.Password + upMap["private_key"] = "" + upMap["pass_phrase"] = "" + } else { + upMap["password"] = "" + upMap["private_key"] = req.PrivateKey + upMap["pass_phrase"] = req.PassPhrase + } + upMap["description"] = req.Description + if err := hostService.Update(req.ID, upMap); err != nil { + helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) + return + } + helper.SuccessWithData(c, nil) +} + +// @Tags Host +// @Summary Update host group +// @Description 切换分组 +// @Accept json +// @Param request body dto.ChangeHostGroup true "request" +// @Success 200 +// @Security ApiKeyAuth +// @Router /hosts/update/group [post] +// @x-panel-log {"bodyKeys":["id","group"],"paramKeys":[],"BeforeFunctions":[{"input_column":"id","input_value":"id","isList":false,"db":"hosts","output_column":"addr","output_value":"addr"}],"formatZH":"切换主机[addr]分组 => [group]","formatEN":"change host [addr] group => [group]"} +func (b *BaseApi) UpdateHostGroup(c *gin.Context) { + var req dto.ChangeHostGroup + if err := helper.CheckBindAndValidate(&req, c); err != nil { + return + } + + upMap := make(map[string]interface{}) + upMap["group_id"] = req.GroupID + if err := hostService.Update(req.ID, upMap); err != nil { + helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) + return + } + helper.SuccessWithData(c, nil) +} diff --git a/agent/app/api/v1/host_tool.go b/agent/app/api/v1/host_tool.go new file mode 100644 index 000000000..6f9a2e84e --- /dev/null +++ b/agent/app/api/v1/host_tool.go @@ -0,0 +1,180 @@ +package v1 + +import ( + "github.com/1Panel-dev/1Panel/agent/app/api/v1/helper" + "github.com/1Panel-dev/1Panel/agent/app/dto/request" + "github.com/1Panel-dev/1Panel/agent/constant" + "github.com/gin-gonic/gin" +) + +// @Tags Host tool +// @Summary Get tool +// @Description 获取主机工具状态 +// @Accept json +// @Param request body request.HostToolReq true "request" +// @Success 200 +// @Security ApiKeyAuth +// @Router /host/tool [post] +func (b *BaseApi) GetToolStatus(c *gin.Context) { + var req request.HostToolReq + if err := helper.CheckBindAndValidate(&req, c); err != nil { + return + } + + config, err := hostToolService.GetToolStatus(req) + if err != nil { + helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) + return + } + helper.SuccessWithData(c, config) +} + +// @Tags Host tool +// @Summary Create Host tool Config +// @Description 创建主机工具配置 +// @Accept json +// @Param request body request.HostToolCreate true "request" +// @Success 200 +// @Security ApiKeyAuth +// @Router /host/tool/create [post] +// @x-panel-log {"bodyKeys":["type"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"创建 [type] 配置","formatEN":"create [type] config"} +func (b *BaseApi) InitToolConfig(c *gin.Context) { + var req request.HostToolCreate + if err := helper.CheckBindAndValidate(&req, c); err != nil { + return + } + + if err := hostToolService.CreateToolConfig(req); err != nil { + helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) + return + } + helper.SuccessWithOutData(c) +} + +// @Tags Host tool +// @Summary Operate tool +// @Description 操作主机工具 +// @Accept json +// @Param request body request.HostToolReq true "request" +// @Success 200 +// @Security ApiKeyAuth +// @Router /host/tool/operate [post] +// @x-panel-log {"bodyKeys":["operate","type"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"[operate] [type] ","formatEN":"[operate] [type]"} +func (b *BaseApi) OperateTool(c *gin.Context) { + var req request.HostToolReq + if err := helper.CheckBindAndValidate(&req, c); err != nil { + return + } + err := hostToolService.OperateTool(req) + if err != nil { + helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) + return + } + helper.SuccessWithOutData(c) +} + +// @Tags Host tool +// @Summary Get tool config +// @Description 操作主机工具配置文件 +// @Accept json +// @Param request body request.HostToolConfig true "request" +// @Success 200 +// @Security ApiKeyAuth +// @Router /host/tool/config [post] +// @x-panel-log {"bodyKeys":["operate"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"[operate] 主机工具配置文件 ","formatEN":"[operate] tool config"} +func (b *BaseApi) OperateToolConfig(c *gin.Context) { + var req request.HostToolConfig + if err := helper.CheckBindAndValidate(&req, c); err != nil { + return + } + + config, err := hostToolService.OperateToolConfig(req) + if err != nil { + helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) + return + } + helper.SuccessWithData(c, config) +} + +// @Tags Host tool +// @Summary Get tool +// @Description 获取主机工具日志 +// @Accept json +// @Param request body request.HostToolLogReq true "request" +// @Success 200 +// @Security ApiKeyAuth +// @Router /host/tool/log [post] +func (b *BaseApi) GetToolLog(c *gin.Context) { + var req request.HostToolLogReq + if err := helper.CheckBindAndValidate(&req, c); err != nil { + return + } + + logContent, err := hostToolService.GetToolLog(req) + if err != nil { + helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) + return + } + helper.SuccessWithData(c, logContent) +} + +// @Tags Host tool +// @Summary Create Supervisor process +// @Description 操作守护进程 +// @Accept json +// @Param request body request.SupervisorProcessConfig true "request" +// @Success 200 +// @Security ApiKeyAuth +// @Router /host/tool/supervisor/process [post] +// @x-panel-log {"bodyKeys":["operate"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"[operate] 守护进程 ","formatEN":"[operate] process"} +func (b *BaseApi) OperateProcess(c *gin.Context) { + var req request.SupervisorProcessConfig + if err := helper.CheckBindAndValidate(&req, c); err != nil { + return + } + + err := hostToolService.OperateSupervisorProcess(req) + if err != nil { + helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) + return + } + helper.SuccessWithOutData(c) +} + +// @Tags Host tool +// @Summary Get Supervisor process config +// @Description 获取 Supervisor 进程配置 +// @Accept json +// @Success 200 +// @Security ApiKeyAuth +// @Router /host/tool/supervisor/process [get] +func (b *BaseApi) GetProcess(c *gin.Context) { + configs, err := hostToolService.GetSupervisorProcessConfig() + if err != nil { + helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) + return + } + helper.SuccessWithData(c, configs) +} + +// @Tags Host tool +// @Summary Get Supervisor process config +// @Description 操作 Supervisor 进程文件 +// @Accept json +// @Param request body request.SupervisorProcessFileReq true "request" +// @Success 200 +// @Security ApiKeyAuth +// @Router /host/tool/supervisor/process/file [post] +// @x-panel-log {"bodyKeys":["operate"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"[operate] Supervisor 进程文件 ","formatEN":"[operate] Supervisor Process Config file"} +func (b *BaseApi) GetProcessFile(c *gin.Context) { + var req request.SupervisorProcessFileReq + if err := helper.CheckBindAndValidate(&req, c); err != nil { + return + } + content, err := hostToolService.OperateSupervisorProcessFile(req) + if err != nil { + helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) + return + } + helper.SuccessWithData(c, content) +} diff --git a/agent/app/api/v1/image.go b/agent/app/api/v1/image.go new file mode 100644 index 000000000..3b24ba6a7 --- /dev/null +++ b/agent/app/api/v1/image.go @@ -0,0 +1,231 @@ +package v1 + +import ( + "github.com/1Panel-dev/1Panel/agent/app/api/v1/helper" + "github.com/1Panel-dev/1Panel/agent/app/dto" + "github.com/1Panel-dev/1Panel/agent/constant" + "github.com/gin-gonic/gin" +) + +// @Tags Container Image +// @Summary Page images +// @Description 获取镜像列表分页 +// @Accept json +// @Param request body dto.SearchWithPage true "request" +// @Produce json +// @Success 200 {object} dto.PageResult +// @Security ApiKeyAuth +// @Router /containers/image/search [post] +func (b *BaseApi) SearchImage(c *gin.Context) { + var req dto.SearchWithPage + if err := helper.CheckBindAndValidate(&req, c); err != nil { + return + } + + total, list, err := imageService.Page(req) + if err != nil { + helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) + return + } + + helper.SuccessWithData(c, dto.PageResult{ + Items: list, + Total: total, + }) +} + +// @Tags Container Image +// @Summary List all images +// @Description 获取所有镜像列表 +// @Produce json +// @Success 200 {array} dto.ImageInfo +// @Security ApiKeyAuth +// @Router /containers/image/all [get] +func (b *BaseApi) ListAllImage(c *gin.Context) { + list, err := imageService.ListAll() + if err != nil { + helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) + return + } + helper.SuccessWithData(c, list) +} + +// @Tags Container Image +// @Summary load images options +// @Description 获取镜像名称列表 +// @Produce json +// @Success 200 {array} dto.Options +// @Security ApiKeyAuth +// @Router /containers/image [get] +func (b *BaseApi) ListImage(c *gin.Context) { + list, err := imageService.List() + if err != nil { + helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) + return + } + helper.SuccessWithData(c, list) +} + +// @Tags Container Image +// @Summary Build image +// @Description 构建镜像 +// @Accept json +// @Param request body dto.ImageBuild true "request" +// @Success 200 {string} log +// @Security ApiKeyAuth +// @Router /containers/image/build [post] +// @x-panel-log {"bodyKeys":["name"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"构建镜像 [name]","formatEN":"build image [name]"} +func (b *BaseApi) ImageBuild(c *gin.Context) { + var req dto.ImageBuild + if err := helper.CheckBindAndValidate(&req, c); err != nil { + return + } + + log, err := imageService.ImageBuild(req) + if err != nil { + helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) + return + } + + helper.SuccessWithData(c, log) +} + +// @Tags Container Image +// @Summary Pull image +// @Description 拉取镜像 +// @Accept json +// @Param request body dto.ImagePull true "request" +// @Success 200 {string} log +// @Security ApiKeyAuth +// @Router /containers/image/pull [post] +// @x-panel-log {"bodyKeys":["repoID","imageName"],"paramKeys":[],"BeforeFunctions":[{"input_column":"id","input_value":"repoID","isList":false,"db":"image_repos","output_column":"name","output_value":"reponame"}],"formatZH":"镜像拉取 [reponame][imageName]","formatEN":"image pull [reponame][imageName]"} +func (b *BaseApi) ImagePull(c *gin.Context) { + var req dto.ImagePull + if err := helper.CheckBindAndValidate(&req, c); err != nil { + return + } + + logPath, err := imageService.ImagePull(req) + if err != nil { + helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) + return + } + + helper.SuccessWithData(c, logPath) +} + +// @Tags Container Image +// @Summary Push image +// @Description 推送镜像 +// @Accept json +// @Param request body dto.ImagePush true "request" +// @Success 200 {string} log +// @Security ApiKeyAuth +// @Router /containers/image/push [post] +// @x-panel-log {"bodyKeys":["repoID","tagName","name"],"paramKeys":[],"BeforeFunctions":[{"input_column":"id","input_value":"repoID","isList":false,"db":"image_repos","output_column":"name","output_value":"reponame"}],"formatZH":"[tagName] 推送到 [reponame][name]","formatEN":"push [tagName] to [reponame][name]"} +func (b *BaseApi) ImagePush(c *gin.Context) { + var req dto.ImagePush + if err := helper.CheckBindAndValidate(&req, c); err != nil { + return + } + + logPath, err := imageService.ImagePush(req) + if err != nil { + helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) + return + } + + helper.SuccessWithData(c, logPath) +} + +// @Tags Container Image +// @Summary Delete image +// @Description 删除镜像 +// @Accept json +// @Param request body dto.BatchDelete true "request" +// @Success 200 +// @Security ApiKeyAuth +// @Router /containers/image/remove [post] +// @x-panel-log {"bodyKeys":["names"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"移除镜像 [names]","formatEN":"remove image [names]"} +func (b *BaseApi) ImageRemove(c *gin.Context) { + var req dto.BatchDelete + if err := helper.CheckBindAndValidate(&req, c); err != nil { + return + } + + if err := imageService.ImageRemove(req); err != nil { + helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) + return + } + + helper.SuccessWithData(c, nil) +} + +// @Tags Container Image +// @Summary Save image +// @Description 导出镜像 +// @Accept json +// @Param request body dto.ImageSave true "request" +// @Success 200 +// @Security ApiKeyAuth +// @Router /containers/image/save [post] +// @x-panel-log {"bodyKeys":["tagName","path","name"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"保留 [tagName] 为 [path]/[name]","formatEN":"save [tagName] as [path]/[name]"} +func (b *BaseApi) ImageSave(c *gin.Context) { + var req dto.ImageSave + if err := helper.CheckBindAndValidate(&req, c); err != nil { + return + } + + if err := imageService.ImageSave(req); err != nil { + helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) + return + } + + helper.SuccessWithData(c, nil) +} + +// @Tags Container Image +// @Summary Tag image +// @Description Tag 镜像 +// @Accept json +// @Param request body dto.ImageTag true "request" +// @Success 200 +// @Security ApiKeyAuth +// @Router /containers/image/tag [post] +// @x-panel-log {"bodyKeys":["repoID","targetName"],"paramKeys":[],"BeforeFunctions":[{"input_column":"id","input_value":"repoID","isList":false,"db":"image_repos","output_column":"name","output_value":"reponame"}],"formatZH":"tag 镜像 [reponame][targetName]","formatEN":"tag image [reponame][targetName]"} +func (b *BaseApi) ImageTag(c *gin.Context) { + var req dto.ImageTag + if err := helper.CheckBindAndValidate(&req, c); err != nil { + return + } + + if err := imageService.ImageTag(req); err != nil { + helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) + return + } + + helper.SuccessWithData(c, nil) +} + +// @Tags Container Image +// @Summary Load image +// @Description 导入镜像 +// @Accept json +// @Param request body dto.ImageLoad true "request" +// @Success 200 +// @Security ApiKeyAuth +// @Router /containers/image/load [post] +// @x-panel-log {"bodyKeys":["path"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"从 [path] 加载镜像","formatEN":"load image from [path]"} +func (b *BaseApi) ImageLoad(c *gin.Context) { + var req dto.ImageLoad + if err := helper.CheckBindAndValidate(&req, c); err != nil { + return + } + + if err := imageService.ImageLoad(req); err != nil { + helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) + return + } + + helper.SuccessWithData(c, nil) +} diff --git a/agent/app/api/v1/image_repo.go b/agent/app/api/v1/image_repo.go new file mode 100644 index 000000000..bd1377c1b --- /dev/null +++ b/agent/app/api/v1/image_repo.go @@ -0,0 +1,143 @@ +package v1 + +import ( + "github.com/1Panel-dev/1Panel/agent/app/api/v1/helper" + "github.com/1Panel-dev/1Panel/agent/app/dto" + "github.com/1Panel-dev/1Panel/agent/constant" + "github.com/gin-gonic/gin" +) + +// @Tags Container Image-repo +// @Summary Page image repos +// @Description 获取镜像仓库列表分页 +// @Accept json +// @Param request body dto.SearchWithPage true "request" +// @Produce json +// @Success 200 {object} dto.PageResult +// @Security ApiKeyAuth +// @Router /containers/repo/search [post] +func (b *BaseApi) SearchRepo(c *gin.Context) { + var req dto.SearchWithPage + if err := helper.CheckBindAndValidate(&req, c); err != nil { + return + } + + total, list, err := imageRepoService.Page(req) + if err != nil { + helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) + return + } + + helper.SuccessWithData(c, dto.PageResult{ + Items: list, + Total: total, + }) +} + +// @Tags Container Image-repo +// @Summary List image repos +// @Description 获取镜像仓库列表 +// @Produce json +// @Success 200 {array} dto.ImageRepoOption +// @Security ApiKeyAuth +// @Router /containers/repo [get] +func (b *BaseApi) ListRepo(c *gin.Context) { + list, err := imageRepoService.List() + if err != nil { + helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) + return + } + + helper.SuccessWithData(c, list) +} + +// @Tags Container Image-repo +// @Summary Load repo status +// @Description 获取 docker 仓库状态 +// @Accept json +// @Param request body dto.OperateByID true "request" +// @Produce json +// @Success 200 +// @Security ApiKeyAuth +// @Router /containers/repo/status [get] +func (b *BaseApi) CheckRepoStatus(c *gin.Context) { + var req dto.OperateByID + if err := helper.CheckBindAndValidate(&req, c); err != nil { + return + } + + if err := imageRepoService.Login(req); err != nil { + helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) + return + } + helper.SuccessWithData(c, nil) +} + +// @Tags Container Image-repo +// @Summary Create image repo +// @Description 创建镜像仓库 +// @Accept json +// @Param request body dto.ImageRepoDelete true "request" +// @Produce json +// @Success 200 +// @Security ApiKeyAuth +// @Router /containers/repo [post] +// @x-panel-log {"bodyKeys":["name"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"创建镜像仓库 [name]","formatEN":"create image repo [name]"} +func (b *BaseApi) CreateRepo(c *gin.Context) { + var req dto.ImageRepoCreate + if err := helper.CheckBindAndValidate(&req, c); err != nil { + return + } + + if err := imageRepoService.Create(req); err != nil { + helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) + return + } + helper.SuccessWithData(c, nil) +} + +// @Tags Container Image-repo +// @Summary Delete image repo +// @Description 删除镜像仓库 +// @Accept json +// @Param request body dto.ImageRepoDelete true "request" +// @Produce json +// @Success 200 +// @Security ApiKeyAuth +// @Router /containers/repo/del [post] +// @x-panel-log {"bodyKeys":["ids"],"paramKeys":[],"BeforeFunctions":[{"input_column":"id","input_value":"ids","isList":true,"db":"image_repos","output_column":"name","output_value":"names"}],"formatZH":"删除镜像仓库 [names]","formatEN":"delete image repo [names]"} +func (b *BaseApi) DeleteRepo(c *gin.Context) { + var req dto.ImageRepoDelete + if err := helper.CheckBindAndValidate(&req, c); err != nil { + return + } + + if err := imageRepoService.BatchDelete(req); err != nil { + helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) + return + } + helper.SuccessWithData(c, nil) +} + +// @Tags Container Image-repo +// @Summary Update image repo +// @Description 更新镜像仓库 +// @Accept json +// @Param request body dto.ImageRepoUpdate true "request" +// @Produce json +// @Success 200 +// @Security ApiKeyAuth +// @Router /containers/repo/update [post] +// @x-panel-log {"bodyKeys":["id"],"paramKeys":[],"BeforeFunctions":[{"input_column":"id","input_value":"id","isList":false,"db":"image_repos","output_column":"name","output_value":"name"}],"formatZH":"更新镜像仓库 [name]","formatEN":"update image repo information [name]"} +func (b *BaseApi) UpdateRepo(c *gin.Context) { + var req dto.ImageRepoUpdate + if err := helper.CheckBindAndValidate(&req, c); err != nil { + return + } + + if err := imageRepoService.Update(req); err != nil { + helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) + return + } + helper.SuccessWithData(c, nil) +} diff --git a/agent/app/api/v1/logs.go b/agent/app/api/v1/logs.go new file mode 100644 index 000000000..dc8873105 --- /dev/null +++ b/agent/app/api/v1/logs.go @@ -0,0 +1,45 @@ +package v1 + +import ( + "github.com/1Panel-dev/1Panel/agent/app/api/v1/helper" + "github.com/1Panel-dev/1Panel/agent/app/dto" + "github.com/1Panel-dev/1Panel/agent/constant" + "github.com/gin-gonic/gin" +) + +// @Tags Logs +// @Summary Load system log files +// @Description 获取系统日志文件列表 +// @Success 200 +// @Security ApiKeyAuth +// @Router /logs/system/files [get] +func (b *BaseApi) GetSystemFiles(c *gin.Context) { + data, err := logService.ListSystemLogFile() + if err != nil { + helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) + return + } + + helper.SuccessWithData(c, data) +} + +// @Tags Logs +// @Summary Load system logs +// @Description 获取系统日志 +// @Success 200 +// @Security ApiKeyAuth +// @Router /logs/system [post] +func (b *BaseApi) GetSystemLogs(c *gin.Context) { + var req dto.OperationWithName + if err := helper.CheckBindAndValidate(&req, c); err != nil { + return + } + + data, err := logService.LoadSystemLog(req.Name) + if err != nil { + helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) + return + } + + helper.SuccessWithData(c, data) +} diff --git a/agent/app/api/v1/monitor.go b/agent/app/api/v1/monitor.go new file mode 100644 index 000000000..33ed2a449 --- /dev/null +++ b/agent/app/api/v1/monitor.go @@ -0,0 +1,134 @@ +package v1 + +import ( + "sort" + "time" + + "github.com/1Panel-dev/1Panel/agent/app/api/v1/helper" + "github.com/1Panel-dev/1Panel/agent/app/dto" + "github.com/1Panel-dev/1Panel/agent/app/model" + "github.com/1Panel-dev/1Panel/agent/constant" + "github.com/1Panel-dev/1Panel/agent/global" + "github.com/1Panel-dev/1Panel/agent/utils/common" + "github.com/gin-gonic/gin" + "github.com/shirou/gopsutil/v3/disk" + "github.com/shirou/gopsutil/v3/net" +) + +// @Tags Monitor +// @Summary Load monitor datas +// @Description 获取监控数据 +// @Param request body dto.MonitorSearch true "request" +// @Success 200 +// @Security ApiKeyAuth +// @Router /hosts/monitor/search [post] +func (b *BaseApi) LoadMonitor(c *gin.Context) { + var req dto.MonitorSearch + if err := helper.CheckBindAndValidate(&req, c); err != nil { + return + } + + loc, _ := time.LoadLocation(common.LoadTimeZone()) + req.StartTime = req.StartTime.In(loc) + req.EndTime = req.EndTime.In(loc) + + var backdatas []dto.MonitorData + if req.Param == "all" || req.Param == "cpu" || req.Param == "memory" || req.Param == "load" { + var bases []model.MonitorBase + if err := global.MonitorDB. + Where("created_at > ? AND created_at < ?", req.StartTime, req.EndTime). + Find(&bases).Error; err != nil { + helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) + return + } + + var itemData dto.MonitorData + itemData.Param = "base" + for _, base := range bases { + itemData.Date = append(itemData.Date, base.CreatedAt) + itemData.Value = append(itemData.Value, base) + } + backdatas = append(backdatas, itemData) + } + if req.Param == "all" || req.Param == "io" { + var bases []model.MonitorIO + if err := global.MonitorDB. + Where("created_at > ? AND created_at < ?", req.StartTime, req.EndTime). + Find(&bases).Error; err != nil { + helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) + return + } + + var itemData dto.MonitorData + itemData.Param = "io" + for _, base := range bases { + itemData.Date = append(itemData.Date, base.CreatedAt) + itemData.Value = append(itemData.Value, base) + } + backdatas = append(backdatas, itemData) + } + if req.Param == "all" || req.Param == "network" { + var bases []model.MonitorNetwork + if err := global.MonitorDB. + Where("name = ? AND created_at > ? AND created_at < ?", req.Info, req.StartTime, req.EndTime). + Find(&bases).Error; err != nil { + helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) + return + } + + var itemData dto.MonitorData + itemData.Param = "network" + for _, base := range bases { + itemData.Date = append(itemData.Date, base.CreatedAt) + itemData.Value = append(itemData.Value, base) + } + backdatas = append(backdatas, itemData) + } + helper.SuccessWithData(c, backdatas) +} + +// @Tags Monitor +// @Summary Clean monitor datas +// @Description 清空监控数据 +// @Success 200 +// @Security ApiKeyAuth +// @Router /hosts/monitor/clean [post] +// @x-panel-log {"bodyKeys":[],"paramKeys":[],"BeforeFunctions":[],"formatZH":"清空监控数据","formatEN":"clean monitor datas"} +func (b *BaseApi) CleanMonitor(c *gin.Context) { + if err := global.MonitorDB.Exec("DELETE FROM monitor_bases").Error; err != nil { + helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) + return + } + if err := global.MonitorDB.Exec("DELETE FROM monitor_ios").Error; err != nil { + helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) + return + } + if err := global.MonitorDB.Exec("DELETE FROM monitor_networks").Error; err != nil { + helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) + return + } + + helper.SuccessWithData(c, nil) +} + +func (b *BaseApi) GetNetworkOptions(c *gin.Context) { + netStat, _ := net.IOCounters(true) + var options []string + options = append(options, "all") + for _, net := range netStat { + options = append(options, net.Name) + } + sort.Strings(options) + helper.SuccessWithData(c, options) +} + +func (b *BaseApi) GetIOOptions(c *gin.Context) { + diskStat, _ := disk.IOCounters() + var options []string + options = append(options, "all") + for _, net := range diskStat { + options = append(options, net.Name) + } + sort.Strings(options) + helper.SuccessWithData(c, options) +} diff --git a/agent/app/api/v1/nginx.go b/agent/app/api/v1/nginx.go new file mode 100644 index 000000000..2d36a3841 --- /dev/null +++ b/agent/app/api/v1/nginx.go @@ -0,0 +1,118 @@ +package v1 + +import ( + "github.com/1Panel-dev/1Panel/agent/app/api/v1/helper" + "github.com/1Panel-dev/1Panel/agent/app/dto/request" + "github.com/1Panel-dev/1Panel/agent/constant" + "github.com/gin-gonic/gin" +) + +// @Tags OpenResty +// @Summary Load OpenResty conf +// @Description 获取 OpenResty 配置信息 +// @Success 200 {object} response.FileInfo +// @Security ApiKeyAuth +// @Router /openresty [get] +func (b *BaseApi) GetNginx(c *gin.Context) { + fileInfo, err := nginxService.GetNginxConfig() + if err != nil { + helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) + return + } + helper.SuccessWithData(c, fileInfo) +} + +// @Tags OpenResty +// @Summary Load partial OpenResty conf +// @Description 获取部分 OpenResty 配置信息 +// @Accept json +// @Param request body request.NginxScopeReq true "request" +// @Success 200 {array} response.NginxParam +// @Security ApiKeyAuth +// @Router /openresty/scope [post] +func (b *BaseApi) GetNginxConfigByScope(c *gin.Context) { + var req request.NginxScopeReq + if err := helper.CheckBindAndValidate(&req, c); err != nil { + return + } + + params, err := nginxService.GetConfigByScope(req) + if err != nil { + helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) + return + } + helper.SuccessWithData(c, params) +} + +// @Tags OpenResty +// @Summary Update OpenResty conf +// @Description 更新 OpenResty 配置信息 +// @Accept json +// @Param request body request.NginxConfigUpdate true "request" +// @Success 200 +// @Security ApiKeyAuth +// @Router /openresty/update [post] +// @x-panel-log {"bodyKeys":["websiteId"],"paramKeys":[],"BeforeFunctions":[{"input_column":"id","input_value":"websiteId","isList":false,"db":"websites","output_column":"primary_domain","output_value":"domain"}],"formatZH":"更新 nginx 配置 [domain]","formatEN":"Update nginx conf [domain]"} +func (b *BaseApi) UpdateNginxConfigByScope(c *gin.Context) { + var req request.NginxConfigUpdate + if err := helper.CheckBindAndValidate(&req, c); err != nil { + return + } + if err := nginxService.UpdateConfigByScope(req); err != nil { + helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) + return + } + helper.SuccessWithData(c, nil) +} + +// @Tags OpenResty +// @Summary Load OpenResty status info +// @Description 获取 OpenResty 状态信息 +// @Success 200 {object} response.NginxStatus +// @Security ApiKeyAuth +// @Router /openresty/status [get] +func (b *BaseApi) GetNginxStatus(c *gin.Context) { + res, err := nginxService.GetStatus() + if err != nil { + helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) + return + } + helper.SuccessWithData(c, res) +} + +// @Tags OpenResty +// @Summary Update OpenResty conf by upload file +// @Description 上传更新 OpenResty 配置文件 +// @Accept json +// @Param request body request.NginxConfigFileUpdate true "request" +// @Success 200 +// @Security ApiKeyAuth +// @Router /openresty/file [post] +// @x-panel-log {"bodyKeys":[],"paramKeys":[],"BeforeFunctions":[],"formatZH":"更新 nginx 配置","formatEN":"Update nginx conf"} +func (b *BaseApi) UpdateNginxFile(c *gin.Context) { + var req request.NginxConfigFileUpdate + if err := helper.CheckBindAndValidate(&req, c); err != nil { + return + } + + if err := nginxService.UpdateConfigFile(req); err != nil { + helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) + return + } + helper.SuccessWithData(c, nil) +} + +// @Tags OpenResty +// @Summary Clear OpenResty proxy cache +// @Description 清理 OpenResty 代理缓存 +// @Success 200 +// @Security ApiKeyAuth +// @Router /openresty/clear [post] +// @x-panel-log {"bodyKeys":[],"paramKeys":[],"BeforeFunctions":[],"formatZH":"清理 Openresty 代理缓存","formatEN":"Clear nginx proxy cache"} +func (b *BaseApi) ClearNginxProxyCache(c *gin.Context) { + if err := nginxService.ClearProxyCache(); err != nil { + helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) + return + } + helper.SuccessWithOutData(c) +} diff --git a/agent/app/api/v1/php_extensions.go b/agent/app/api/v1/php_extensions.go new file mode 100644 index 000000000..9103af2a6 --- /dev/null +++ b/agent/app/api/v1/php_extensions.go @@ -0,0 +1,103 @@ +package v1 + +import ( + "github.com/1Panel-dev/1Panel/agent/app/api/v1/helper" + "github.com/1Panel-dev/1Panel/agent/app/dto" + "github.com/1Panel-dev/1Panel/agent/app/dto/request" + "github.com/1Panel-dev/1Panel/agent/constant" + "github.com/gin-gonic/gin" +) + +// @Tags PHP Extensions +// @Summary Page Extensions +// @Description Page Extensions +// @Accept json +// @Param request body request.PHPExtensionsSearch true "request" +// @Success 200 {array} response.PHPExtensionsDTO +// @Security ApiKeyAuth +// @Router /runtimes/php/extensions/search [post] +func (b *BaseApi) PagePHPExtensions(c *gin.Context) { + var req request.PHPExtensionsSearch + if err := helper.CheckBindAndValidate(&req, c); err != nil { + return + } + if req.All { + list, err := phpExtensionsService.List() + if err != nil { + helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) + return + } + helper.SuccessWithData(c, list) + } else { + total, list, err := phpExtensionsService.Page(req) + if err != nil { + helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) + return + } + helper.SuccessWithData(c, dto.PageResult{ + Total: total, + Items: list, + }) + } + +} + +// @Tags PHP Extensions +// @Summary Create Extensions +// @Description Create Extensions +// @Accept json +// @Param request body request.PHPExtensionsCreate true "request" +// @Success 200 +// @Security ApiKeyAuth +// @Router /runtimes/php/extensions [post] +func (b *BaseApi) CreatePHPExtensions(c *gin.Context) { + var req request.PHPExtensionsCreate + if err := helper.CheckBindAndValidate(&req, c); err != nil { + return + } + if err := phpExtensionsService.Create(req); err != nil { + helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) + return + } + helper.SuccessWithOutData(c) +} + +// @Tags PHP Extensions +// @Summary Update Extensions +// @Description Update Extensions +// @Accept json +// @Param request body request.PHPExtensionsUpdate true "request" +// @Success 200 +// @Security ApiKeyAuth +// @Router /runtimes/php/extensions/update [post] +func (b *BaseApi) UpdatePHPExtensions(c *gin.Context) { + var req request.PHPExtensionsUpdate + if err := helper.CheckBindAndValidate(&req, c); err != nil { + return + } + if err := phpExtensionsService.Update(req); err != nil { + helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) + return + } + helper.SuccessWithOutData(c) +} + +// @Tags PHP Extensions +// @Summary Delete Extensions +// @Description Delete Extensions +// @Accept json +// @Param request body request.PHPExtensionsDelete true "request" +// @Success 200 +// @Security ApiKeyAuth +// @Router /runtimes/php/extensions/del [post] +func (b *BaseApi) DeletePHPExtensions(c *gin.Context) { + var req request.PHPExtensionsDelete + if err := helper.CheckBindAndValidate(&req, c); err != nil { + return + } + if err := phpExtensionsService.Delete(req); err != nil { + helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) + return + } + helper.SuccessWithOutData(c) +} diff --git a/agent/app/api/v1/process.go b/agent/app/api/v1/process.go new file mode 100644 index 000000000..8b2e57c96 --- /dev/null +++ b/agent/app/api/v1/process.go @@ -0,0 +1,39 @@ +package v1 + +import ( + "github.com/1Panel-dev/1Panel/agent/app/api/v1/helper" + "github.com/1Panel-dev/1Panel/agent/app/dto/request" + "github.com/1Panel-dev/1Panel/agent/constant" + websocket2 "github.com/1Panel-dev/1Panel/agent/utils/websocket" + "github.com/gin-gonic/gin" +) + +func (b *BaseApi) ProcessWs(c *gin.Context) { + ws, err := wsUpgrade.Upgrade(c.Writer, c.Request, nil) + if err != nil { + return + } + wsClient := websocket2.NewWsClient("processClient", ws) + go wsClient.Read() + go wsClient.Write() +} + +// @Tags Process +// @Summary Stop Process +// @Description 停止进程 +// @Param request body request.ProcessReq true "request" +// @Success 200 +// @Security ApiKeyAuth +// @Router /process/stop [post] +// @x-panel-log {"bodyKeys":["PID"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"结束进程 [PID]","formatEN":"结束进程 [PID]"} +func (b *BaseApi) StopProcess(c *gin.Context) { + var req request.ProcessReq + if err := helper.CheckBindAndValidate(&req, c); err != nil { + return + } + if err := processService.StopProcess(req); err != nil { + helper.ErrorWithDetail(c, constant.CodeErrBadRequest, constant.ErrTypeInvalidParams, err) + return + } + helper.SuccessWithOutData(c) +} diff --git a/agent/app/api/v1/recycle_bin.go b/agent/app/api/v1/recycle_bin.go new file mode 100644 index 000000000..eed594386 --- /dev/null +++ b/agent/app/api/v1/recycle_bin.go @@ -0,0 +1,86 @@ +package v1 + +import ( + "github.com/1Panel-dev/1Panel/agent/app/api/v1/helper" + "github.com/1Panel-dev/1Panel/agent/app/dto" + "github.com/1Panel-dev/1Panel/agent/app/dto/request" + "github.com/1Panel-dev/1Panel/agent/constant" + "github.com/gin-gonic/gin" +) + +// @Tags File +// @Summary List RecycleBin files +// @Description 获取回收站文件列表 +// @Accept json +// @Param request body dto.PageInfo true "request" +// @Success 200 +// @Security ApiKeyAuth +// @Router /files/recycle/search [post] +func (b *BaseApi) SearchRecycleBinFile(c *gin.Context) { + var req dto.PageInfo + if err := helper.CheckBindAndValidate(&req, c); err != nil { + return + } + total, list, err := recycleBinService.Page(req) + if err != nil { + helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) + return + } + helper.SuccessWithData(c, dto.PageResult{ + Items: list, + Total: total, + }) +} + +// @Tags File +// @Summary Reduce RecycleBin files +// @Description 还原回收站文件 +// @Accept json +// @Param request body request.RecycleBinReduce true "request" +// @Success 200 +// @Security ApiKeyAuth +// @Router /files/recycle/reduce [post] +// @x-panel-log {"bodyKeys":["name"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"还原回收站文件 [name]","formatEN":"Reduce RecycleBin file [name]"} +func (b *BaseApi) ReduceRecycleBinFile(c *gin.Context) { + var req request.RecycleBinReduce + if err := helper.CheckBindAndValidate(&req, c); err != nil { + return + } + if err := recycleBinService.Reduce(req); err != nil { + helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) + return + } + helper.SuccessWithOutData(c) +} + +// @Tags File +// @Summary Clear RecycleBin files +// @Description 清空回收站文件 +// @Accept json +// @Success 200 +// @Security ApiKeyAuth +// @Router /files/recycle/clear [post] +// @x-panel-log {"bodyKeys":[],"paramKeys":[],"BeforeFunctions":[],"formatZH":"清空回收站","formatEN":"清空回收站"} +func (b *BaseApi) ClearRecycleBinFile(c *gin.Context) { + if err := recycleBinService.Clear(); err != nil { + helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) + return + } + helper.SuccessWithOutData(c) +} + +// @Tags File +// @Summary Get RecycleBin status +// @Description 获取回收站状态 +// @Accept json +// @Success 200 +// @Security ApiKeyAuth +// @Router /files/recycle/status [get] +func (b *BaseApi) GetRecycleStatus(c *gin.Context) { + settingInfo, err := settingService.GetSettingInfo() + if err != nil { + helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) + return + } + helper.SuccessWithData(c, settingInfo.FileRecycleBin) +} diff --git a/agent/app/api/v1/runtime.go b/agent/app/api/v1/runtime.go new file mode 100644 index 000000000..dd8522130 --- /dev/null +++ b/agent/app/api/v1/runtime.go @@ -0,0 +1,235 @@ +package v1 + +import ( + "github.com/1Panel-dev/1Panel/agent/app/api/v1/helper" + "github.com/1Panel-dev/1Panel/agent/app/dto" + "github.com/1Panel-dev/1Panel/agent/app/dto/request" + "github.com/1Panel-dev/1Panel/agent/constant" + "github.com/gin-gonic/gin" +) + +// @Tags Runtime +// @Summary List runtimes +// @Description 获取运行环境列表 +// @Accept json +// @Param request body request.RuntimeSearch true "request" +// @Success 200 +// @Security ApiKeyAuth +// @Router /runtimes/search [post] +func (b *BaseApi) SearchRuntimes(c *gin.Context) { + var req request.RuntimeSearch + if err := helper.CheckBindAndValidate(&req, c); err != nil { + return + } + total, items, err := runtimeService.Page(req) + if err != nil { + helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) + return + } + helper.SuccessWithData(c, dto.PageResult{ + Total: total, + Items: items, + }) +} + +// @Tags Runtime +// @Summary Create runtime +// @Description 创建运行环境 +// @Accept json +// @Param request body request.RuntimeCreate true "request" +// @Success 200 +// @Security ApiKeyAuth +// @Router /runtimes [post] +// @x-panel-log {"bodyKeys":["name"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"创建运行环境 [name]","formatEN":"Create runtime [name]"} +func (b *BaseApi) CreateRuntime(c *gin.Context) { + var req request.RuntimeCreate + if err := helper.CheckBindAndValidate(&req, c); err != nil { + return + } + ssl, err := runtimeService.Create(req) + if err != nil { + helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) + return + } + helper.SuccessWithData(c, ssl) +} + +// @Tags Website +// @Summary Delete runtime +// @Description 删除运行环境 +// @Accept json +// @Param request body request.RuntimeDelete true "request" +// @Success 200 +// @Security ApiKeyAuth +// @Router /runtimes/del [post] +// @x-panel-log {"bodyKeys":["id"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"删除网站 [name]","formatEN":"Delete website [name]"} +func (b *BaseApi) DeleteRuntime(c *gin.Context) { + var req request.RuntimeDelete + if err := helper.CheckBindAndValidate(&req, c); err != nil { + return + } + err := runtimeService.Delete(req) + if err != nil { + helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) + return + } + helper.SuccessWithOutData(c) +} + +func (b *BaseApi) DeleteRuntimeCheck(c *gin.Context) { + runTimeId, err := helper.GetIntParamByKey(c, "runTimeId") + if err != nil { + helper.ErrorWithDetail(c, constant.CodeErrBadRequest, constant.ErrTypeInternalServer, nil) + return + } + checkData, err := runtimeService.DeleteCheck(runTimeId) + if err != nil { + helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) + return + } + helper.SuccessWithData(c, checkData) +} + +// @Tags Runtime +// @Summary Update runtime +// @Description 更新运行环境 +// @Accept json +// @Param request body request.RuntimeUpdate true "request" +// @Success 200 +// @Security ApiKeyAuth +// @Router /runtimes/update [post] +// @x-panel-log {"bodyKeys":["name"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"更新运行环境 [name]","formatEN":"Update runtime [name]"} +func (b *BaseApi) UpdateRuntime(c *gin.Context) { + var req request.RuntimeUpdate + if err := helper.CheckBindAndValidate(&req, c); err != nil { + return + } + if err := runtimeService.Update(req); err != nil { + helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) + return + } + helper.SuccessWithOutData(c) +} + +// @Tags Runtime +// @Summary Get runtime +// @Description 获取运行环境 +// @Accept json +// @Param id path string true "request" +// @Success 200 +// @Security ApiKeyAuth +// @Router /runtimes/:id [get] +func (b *BaseApi) GetRuntime(c *gin.Context) { + id, err := helper.GetIntParamByKey(c, "id") + if err != nil { + helper.ErrorWithDetail(c, constant.CodeErrBadRequest, constant.ErrTypeInternalServer, nil) + return + } + res, err := runtimeService.Get(id) + if err != nil { + helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) + return + } + helper.SuccessWithData(c, res) +} + +// @Tags Runtime +// @Summary Get Node package scripts +// @Description 获取 Node 项目的 scripts +// @Accept json +// @Param request body request.NodePackageReq true "request" +// @Success 200 +// @Security ApiKeyAuth +// @Router /runtimes/node/package [post] +func (b *BaseApi) GetNodePackageRunScript(c *gin.Context) { + var req request.NodePackageReq + if err := helper.CheckBindAndValidate(&req, c); err != nil { + return + } + res, err := runtimeService.GetNodePackageRunScript(req) + if err != nil { + helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) + return + } + helper.SuccessWithData(c, res) +} + +// @Tags Runtime +// @Summary Operate runtime +// @Description 操作运行环境 +// @Accept json +// @Param request body request.RuntimeOperate true "request" +// @Success 200 +// @Security ApiKeyAuth +// @Router /runtimes/operate [post] +// @x-panel-log {"bodyKeys":["id"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"操作运行环境 [name]","formatEN":"Operate runtime [name]"} +func (b *BaseApi) OperateRuntime(c *gin.Context) { + var req request.RuntimeOperate + if err := helper.CheckBindAndValidate(&req, c); err != nil { + return + } + err := runtimeService.OperateRuntime(req) + if err != nil { + helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) + return + } + helper.SuccessWithOutData(c) +} + +// @Tags Runtime +// @Summary Get Node modules +// @Description 获取 Node 项目的 modules +// @Accept json +// @Param request body request.NodeModuleReq true "request" +// @Success 200 +// @Security ApiKeyAuth +// @Router /runtimes/node/modules [post] +func (b *BaseApi) GetNodeModules(c *gin.Context) { + var req request.NodeModuleReq + if err := helper.CheckBindAndValidate(&req, c); err != nil { + return + } + res, err := runtimeService.GetNodeModules(req) + if err != nil { + helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) + return + } + helper.SuccessWithData(c, res) +} + +// @Tags Runtime +// @Summary Operate Node modules +// @Description 操作 Node 项目 modules +// @Accept json +// @Param request body request.NodeModuleReq true "request" +// @Success 200 +// @Security ApiKeyAuth +// @Router /runtimes/node/modules/operate [post] +func (b *BaseApi) OperateNodeModules(c *gin.Context) { + var req request.NodeModuleOperateReq + if err := helper.CheckBindAndValidate(&req, c); err != nil { + return + } + err := runtimeService.OperateNodeModules(req) + if err != nil { + helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) + return + } + 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) +} diff --git a/agent/app/api/v1/setting.go b/agent/app/api/v1/setting.go new file mode 100644 index 000000000..a6425cb2e --- /dev/null +++ b/agent/app/api/v1/setting.go @@ -0,0 +1,66 @@ +package v1 + +import ( + "github.com/1Panel-dev/1Panel/agent/app/api/v1/helper" + "github.com/1Panel-dev/1Panel/agent/app/dto" + "github.com/1Panel-dev/1Panel/agent/constant" + "github.com/1Panel-dev/1Panel/agent/global" + "github.com/gin-gonic/gin" +) + +// @Tags System Setting +// @Summary Load system setting info +// @Description 加载系统配置信息 +// @Success 200 {object} dto.SettingInfo +// @Security ApiKeyAuth +// @Router /settings/search [post] +func (b *BaseApi) GetSettingInfo(c *gin.Context) { + setting, err := settingService.GetSettingInfo() + if err != nil { + helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) + return + } + helper.SuccessWithData(c, setting) +} + +// @Tags System Setting +// @Summary Load system available status +// @Description 获取系统可用状态 +// @Success 200 +// @Security ApiKeyAuth +// @Router /settings/search/available [get] +func (b *BaseApi) GetSystemAvailable(c *gin.Context) { + helper.SuccessWithData(c, nil) +} + +// @Tags System Setting +// @Summary Update system setting +// @Description 更新系统配置 +// @Accept json +// @Param request body dto.SettingUpdate true "request" +// @Success 200 +// @Security ApiKeyAuth +// @Router /settings/update [post] +// @x-panel-log {"bodyKeys":["key","value"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"修改系统配置 [key] => [value]","formatEN":"update system setting [key] => [value]"} +func (b *BaseApi) UpdateSetting(c *gin.Context) { + var req dto.SettingUpdate + if err := helper.CheckBindAndValidate(&req, c); err != nil { + return + } + + if err := settingService.Update(req.Key, req.Value); err != nil { + helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) + return + } + helper.SuccessWithData(c, nil) +} + +// @Tags System Setting +// @Summary Load local backup dir +// @Description 获取安装根目录 +// @Success 200 {string} path +// @Security ApiKeyAuth +// @Router /settings/basedir [get] +func (b *BaseApi) LoadBaseDir(c *gin.Context) { + helper.SuccessWithData(c, global.CONF.System.DataDir) +} diff --git a/agent/app/api/v1/snapshot.go b/agent/app/api/v1/snapshot.go new file mode 100644 index 000000000..7f4adfb1f --- /dev/null +++ b/agent/app/api/v1/snapshot.go @@ -0,0 +1,187 @@ +package v1 + +import ( + "github.com/1Panel-dev/1Panel/agent/app/api/v1/helper" + "github.com/1Panel-dev/1Panel/agent/app/dto" + "github.com/1Panel-dev/1Panel/agent/constant" + "github.com/gin-gonic/gin" +) + +// @Tags System Setting +// @Summary Create system snapshot +// @Description 创建系统快照 +// @Accept json +// @Param request body dto.SnapshotCreate true "request" +// @Success 200 +// @Security ApiKeyAuth +// @Router /settings/snapshot [post] +// @x-panel-log {"bodyKeys":["from", "description"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"创建系统快照 [description] 到 [from]","formatEN":"Create system backup [description] to [from]"} +func (b *BaseApi) CreateSnapshot(c *gin.Context) { + var req dto.SnapshotCreate + if err := helper.CheckBindAndValidate(&req, c); err != nil { + return + } + + if err := snapshotService.SnapshotCreate(req); err != nil { + helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) + return + } + helper.SuccessWithData(c, nil) +} + +// @Tags System Setting +// @Summary Import system snapshot +// @Description 导入已有快照 +// @Accept json +// @Param request body dto.SnapshotImport true "request" +// @Success 200 +// @Security ApiKeyAuth +// @Router /settings/snapshot/import [post] +// @x-panel-log {"bodyKeys":["from", "names"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"从 [from] 同步系统快照 [names]","formatEN":"Sync system snapshots [names] from [from]"} +func (b *BaseApi) ImportSnapshot(c *gin.Context) { + var req dto.SnapshotImport + if err := helper.CheckBindAndValidate(&req, c); err != nil { + return + } + + if err := snapshotService.SnapshotImport(req); err != nil { + helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) + return + } + helper.SuccessWithData(c, nil) +} + +// @Tags System Setting +// @Summary Load Snapshot status +// @Description 获取快照状态 +// @Accept json +// @Param request body dto.OperateByID true "request" +// @Success 200 +// @Security ApiKeyAuth +// @Router /settings/snapshot/status [post] +func (b *BaseApi) LoadSnapShotStatus(c *gin.Context) { + var req dto.OperateByID + if err := helper.CheckBindAndValidate(&req, c); err != nil { + return + } + + data, err := snapshotService.LoadSnapShotStatus(req.ID) + if err != nil { + helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) + return + } + helper.SuccessWithData(c, data) +} + +// @Tags System Setting +// @Summary Update snapshot description +// @Description 更新快照描述信息 +// @Accept json +// @Param request body dto.UpdateDescription true "request" +// @Success 200 +// @Security ApiKeyAuth +// @Router /settings/snapshot/description/update [post] +// @x-panel-log {"bodyKeys":["id","description"],"paramKeys":[],"BeforeFunctions":[{"input_column":"id","input_value":"id","isList":false,"db":"snapshots","output_column":"name","output_value":"name"}],"formatZH":"快照 [name] 描述信息修改 [description]","formatEN":"The description of the snapshot [name] is modified => [description]"} +func (b *BaseApi) UpdateSnapDescription(c *gin.Context) { + var req dto.UpdateDescription + if err := helper.CheckBindAndValidate(&req, c); err != nil { + return + } + + if err := snapshotService.UpdateDescription(req); err != nil { + helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) + return + } + helper.SuccessWithData(c, nil) +} + +// @Tags System Setting +// @Summary Page system snapshot +// @Description 获取系统快照列表分页 +// @Accept json +// @Param request body dto.SearchWithPage true "request" +// @Success 200 {object} dto.PageResult +// @Security ApiKeyAuth +// @Router /settings/snapshot/search [post] +func (b *BaseApi) SearchSnapshot(c *gin.Context) { + var req dto.SearchWithPage + if err := helper.CheckBindAndValidate(&req, c); err != nil { + return + } + + total, accounts, err := snapshotService.SearchWithPage(req) + if err != nil { + helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) + return + } + helper.SuccessWithData(c, dto.PageResult{ + Total: total, + Items: accounts, + }) +} + +// @Tags System Setting +// @Summary Recover system backup +// @Description 从系统快照恢复 +// @Accept json +// @Param request body dto.SnapshotRecover true "request" +// @Success 200 +// @Security ApiKeyAuth +// @Router /settings/snapshot/recover [post] +// @x-panel-log {"bodyKeys":["id"],"paramKeys":[],"BeforeFunctions":[{"input_column":"id","input_value":"id","isList":false,"db":"snapshots","output_column":"name","output_value":"name"}],"formatZH":"从系统快照 [name] 恢复","formatEN":"Recover from system backup [name]"} +func (b *BaseApi) RecoverSnapshot(c *gin.Context) { + var req dto.SnapshotRecover + if err := helper.CheckBindAndValidate(&req, c); err != nil { + return + } + + if err := snapshotService.SnapshotRecover(req); err != nil { + helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) + return + } + helper.SuccessWithData(c, nil) +} + +// @Tags System Setting +// @Summary Rollback system backup +// @Description 从系统快照回滚 +// @Accept json +// @Param request body dto.SnapshotRecover true "request" +// @Success 200 +// @Security ApiKeyAuth +// @Router /settings/snapshot/rollback [post] +// @x-panel-log {"bodyKeys":["id"],"paramKeys":[],"BeforeFunctions":[{"input_column":"id","input_value":"id","isList":false,"db":"snapshots","output_column":"name","output_value":"name"}],"formatZH":"从系统快照 [name] 回滚","formatEN":"Rollback from system backup [name]"} +func (b *BaseApi) RollbackSnapshot(c *gin.Context) { + var req dto.SnapshotRecover + if err := helper.CheckBindAndValidate(&req, c); err != nil { + return + } + + if err := snapshotService.SnapshotRollback(req); err != nil { + helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) + return + } + helper.SuccessWithData(c, nil) +} + +// @Tags System Setting +// @Summary Delete system backup +// @Description 删除系统快照 +// @Accept json +// @Param request body dto.SnapshotBatchDelete true "request" +// @Success 200 +// @Security ApiKeyAuth +// @Router /settings/snapshot/del [post] +// @x-panel-log {"bodyKeys":["ids"],"paramKeys":[],"BeforeFunctions":[{"input_column":"id","input_value":"ids","isList":true,"db":"snapshots","output_column":"name","output_value":"name"}],"formatZH":"删除系统快照 [name]","formatEN":"Delete system backup [name]"} +func (b *BaseApi) DeleteSnapshot(c *gin.Context) { + var req dto.SnapshotBatchDelete + if err := helper.CheckBindAndValidate(&req, c); err != nil { + return + } + + if err := snapshotService.Delete(req); err != nil { + helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) + return + } + helper.SuccessWithData(c, nil) +} diff --git a/agent/app/api/v1/ssh.go b/agent/app/api/v1/ssh.go new file mode 100644 index 000000000..7f6ae6458 --- /dev/null +++ b/agent/app/api/v1/ssh.go @@ -0,0 +1,169 @@ +package v1 + +import ( + "github.com/1Panel-dev/1Panel/agent/app/api/v1/helper" + "github.com/1Panel-dev/1Panel/agent/app/dto" + "github.com/1Panel-dev/1Panel/agent/constant" + "github.com/gin-gonic/gin" +) + +// @Tags SSH +// @Summary Load host SSH setting info +// @Description 加载 SSH 配置信息 +// @Success 200 {object} dto.SSHInfo +// @Security ApiKeyAuth +// @Router /host/ssh/search [post] +func (b *BaseApi) GetSSHInfo(c *gin.Context) { + info, err := sshService.GetSSHInfo() + if err != nil { + helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) + return + } + helper.SuccessWithData(c, info) +} + +// @Tags SSH +// @Summary Operate SSH +// @Description 修改 SSH 服务状态 +// @Accept json +// @Param request body dto.Operate true "request" +// @Security ApiKeyAuth +// @Router /host/ssh/operate [post] +// @x-panel-log {"bodyKeys":["operation"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"[operation] SSH ","formatEN":"[operation] SSH"} +func (b *BaseApi) OperateSSH(c *gin.Context) { + var req dto.Operate + if err := helper.CheckBindAndValidate(&req, c); err != nil { + return + } + + if err := sshService.OperateSSH(req.Operation); err != nil { + helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) + return + } + helper.SuccessWithData(c, nil) +} + +// @Tags SSH +// @Summary Update host SSH setting +// @Description 更新 SSH 配置 +// @Accept json +// @Param request body dto.SSHUpdate true "request" +// @Success 200 +// @Security ApiKeyAuth +// @Router /host/ssh/update [post] +// @x-panel-log {"bodyKeys":["key","value"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"修改 SSH 配置 [key] => [value]","formatEN":"update SSH setting [key] => [value]"} +func (b *BaseApi) UpdateSSH(c *gin.Context) { + var req dto.SSHUpdate + if err := helper.CheckBindAndValidate(&req, c); err != nil { + return + } + + if err := sshService.Update(req); err != nil { + helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) + return + } + helper.SuccessWithData(c, nil) +} + +// @Tags SSH +// @Summary Update host SSH setting by file +// @Description 上传文件更新 SSH 配置 +// @Accept json +// @Param request body dto.SSHConf true "request" +// @Success 200 +// @Security ApiKeyAuth +// @Router /host/conffile/update [post] +// @x-panel-log {"bodyKeys":[],"paramKeys":[],"BeforeFunctions":[],"formatZH":"修改 SSH 配置文件","formatEN":"update SSH conf"} +func (b *BaseApi) UpdateSSHByfile(c *gin.Context) { + var req dto.SSHConf + if err := helper.CheckBindAndValidate(&req, c); err != nil { + return + } + + if err := sshService.UpdateByFile(req.File); err != nil { + helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) + return + } + helper.SuccessWithData(c, nil) +} + +// @Tags SSH +// @Summary Generate host SSH secret +// @Description 生成 SSH 密钥 +// @Accept json +// @Param request body dto.GenerateSSH true "request" +// @Success 200 +// @Security ApiKeyAuth +// @Router /host/ssh/generate [post] +// @x-panel-log {"bodyKeys":[],"paramKeys":[],"BeforeFunctions":[],"formatZH":"生成 SSH 密钥 ","formatEN":"generate SSH secret"} +func (b *BaseApi) GenerateSSH(c *gin.Context) { + var req dto.GenerateSSH + if err := helper.CheckBindAndValidate(&req, c); err != nil { + return + } + + if err := sshService.GenerateSSH(req); err != nil { + helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) + return + } + helper.SuccessWithData(c, nil) +} + +// @Tags SSH +// @Summary Load host SSH secret +// @Description 获取 SSH 密钥 +// @Accept json +// @Param request body dto.GenerateLoad true "request" +// @Success 200 +// @Security ApiKeyAuth +// @Router /host/ssh/secret [post] +func (b *BaseApi) LoadSSHSecret(c *gin.Context) { + var req dto.GenerateLoad + if err := helper.CheckBindAndValidate(&req, c); err != nil { + return + } + + data, err := sshService.LoadSSHSecret(req.EncryptionMode) + if err != nil { + helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) + return + } + helper.SuccessWithData(c, data) +} + +// @Tags SSH +// @Summary Load host SSH logs +// @Description 获取 SSH 登录日志 +// @Accept json +// @Param request body dto.SearchSSHLog true "request" +// @Success 200 {object} dto.SSHLog +// @Security ApiKeyAuth +// @Router /host/ssh/log [post] +func (b *BaseApi) LoadSSHLogs(c *gin.Context) { + var req dto.SearchSSHLog + if err := helper.CheckBindAndValidate(&req, c); err != nil { + return + } + + data, err := sshService.LoadLog(req) + if err != nil { + helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) + return + } + helper.SuccessWithData(c, data) +} + +// @Tags SSH +// @Summary Load host SSH conf +// @Description 获取 SSH 配置文件 +// @Success 200 +// @Security ApiKeyAuth +// @Router /host/ssh/conf [get] +func (b *BaseApi) LoadSSHConf(c *gin.Context) { + data, err := sshService.LoadSSHConf() + if err != nil { + helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) + return + } + helper.SuccessWithData(c, data) +} diff --git a/agent/app/api/v1/terminal.go b/agent/app/api/v1/terminal.go new file mode 100644 index 000000000..cedac00a0 --- /dev/null +++ b/agent/app/api/v1/terminal.go @@ -0,0 +1,285 @@ +package v1 + +import ( + "encoding/base64" + "encoding/json" + "fmt" + "net/http" + "strconv" + "strings" + "time" + + "github.com/1Panel-dev/1Panel/agent/app/dto" + "github.com/1Panel-dev/1Panel/agent/global" + "github.com/1Panel-dev/1Panel/agent/utils/cmd" + "github.com/1Panel-dev/1Panel/agent/utils/copier" + "github.com/1Panel-dev/1Panel/agent/utils/ssh" + "github.com/1Panel-dev/1Panel/agent/utils/terminal" + "github.com/gin-gonic/gin" + "github.com/gorilla/websocket" + "github.com/pkg/errors" +) + +func (b *BaseApi) WsSsh(c *gin.Context) { + wsConn, err := upGrader.Upgrade(c.Writer, c.Request, nil) + if err != nil { + global.LOG.Errorf("gin context http handler failed, err: %v", err) + return + } + defer wsConn.Close() + + id, err := strconv.Atoi(c.Query("id")) + if wshandleError(wsConn, errors.WithMessage(err, "invalid param id in request")) { + return + } + cols, err := strconv.Atoi(c.DefaultQuery("cols", "80")) + if wshandleError(wsConn, errors.WithMessage(err, "invalid param cols in request")) { + return + } + rows, err := strconv.Atoi(c.DefaultQuery("rows", "40")) + if wshandleError(wsConn, errors.WithMessage(err, "invalid param rows in request")) { + return + } + host, err := hostService.GetHostInfo(uint(id)) + if wshandleError(wsConn, errors.WithMessage(err, "load host info by id failed")) { + return + } + var connInfo ssh.ConnInfo + _ = copier.Copy(&connInfo, &host) + connInfo.PrivateKey = []byte(host.PrivateKey) + if len(host.PassPhrase) != 0 { + connInfo.PassPhrase = []byte(host.PassPhrase) + } + + client, err := connInfo.NewClient() + if wshandleError(wsConn, errors.WithMessage(err, "failed to set up the connection. Please check the host information")) { + return + } + defer client.Close() + sws, err := terminal.NewLogicSshWsSession(cols, rows, true, connInfo.Client, wsConn) + if wshandleError(wsConn, err) { + return + } + defer sws.Close() + + quitChan := make(chan bool, 3) + sws.Start(quitChan) + go sws.Wait(quitChan) + + <-quitChan + + if wshandleError(wsConn, err) { + return + } +} + +func (b *BaseApi) RedisWsSsh(c *gin.Context) { + wsConn, err := upGrader.Upgrade(c.Writer, c.Request, nil) + if err != nil { + global.LOG.Errorf("gin context http handler failed, err: %v", err) + return + } + defer wsConn.Close() + + if global.CONF.System.IsDemo { + if wshandleError(wsConn, errors.New(" demo server, prohibit this operation!")) { + return + } + } + + cols, err := strconv.Atoi(c.DefaultQuery("cols", "80")) + if wshandleError(wsConn, errors.WithMessage(err, "invalid param cols in request")) { + return + } + rows, err := strconv.Atoi(c.DefaultQuery("rows", "40")) + if wshandleError(wsConn, errors.WithMessage(err, "invalid param rows in request")) { + return + } + name := c.Query("name") + from := c.Query("from") + commands := []string{"redis-cli"} + database, err := databaseService.Get(name) + if wshandleError(wsConn, errors.WithMessage(err, "no such database in db")) { + return + } + if from == "local" { + redisInfo, err := appInstallService.LoadConnInfo(dto.OperationWithNameAndType{Name: name, Type: "redis"}) + if wshandleError(wsConn, errors.WithMessage(err, "no such database in db")) { + return + } + name = redisInfo.ContainerName + if len(database.Password) != 0 { + commands = []string{"redis-cli", "-a", database.Password, "--no-auth-warning"} + } + } else { + itemPort := fmt.Sprintf("%v", database.Port) + commands = []string{"redis-cli", "-h", database.Address, "-p", itemPort} + if len(database.Password) != 0 { + commands = []string{"redis-cli", "-h", database.Address, "-p", itemPort, "-a", database.Password, "--no-auth-warning"} + } + name = "1Panel-redis-cli-tools" + } + + pidMap := loadMapFromDockerTop(name) + itemCmds := append([]string{"exec", "-it", name}, commands...) + slave, err := terminal.NewCommand(itemCmds) + if wshandleError(wsConn, err) { + return + } + defer killBash(name, strings.Join(commands, " "), pidMap) + defer slave.Close() + + tty, err := terminal.NewLocalWsSession(cols, rows, wsConn, slave, false) + if wshandleError(wsConn, err) { + return + } + + quitChan := make(chan bool, 3) + tty.Start(quitChan) + go slave.Wait(quitChan) + + <-quitChan + + global.LOG.Info("websocket finished") + if wshandleError(wsConn, err) { + return + } +} + +func (b *BaseApi) ContainerWsSsh(c *gin.Context) { + wsConn, err := upGrader.Upgrade(c.Writer, c.Request, nil) + if err != nil { + global.LOG.Errorf("gin context http handler failed, err: %v", err) + return + } + defer wsConn.Close() + + if global.CONF.System.IsDemo { + if wshandleError(wsConn, errors.New(" demo server, prohibit this operation!")) { + return + } + } + + containerID := c.Query("containerid") + command := c.Query("command") + user := c.Query("user") + if len(command) == 0 || len(containerID) == 0 { + if wshandleError(wsConn, errors.New("error param of command or containerID")) { + return + } + } + cols, err := strconv.Atoi(c.DefaultQuery("cols", "80")) + if wshandleError(wsConn, errors.WithMessage(err, "invalid param cols in request")) { + return + } + rows, err := strconv.Atoi(c.DefaultQuery("rows", "40")) + if wshandleError(wsConn, errors.WithMessage(err, "invalid param rows in request")) { + return + } + + cmds := []string{"exec", containerID, command} + if len(user) != 0 { + cmds = []string{"exec", "-u", user, containerID, command} + } + if cmd.CheckIllegal(user, containerID, command) { + if wshandleError(wsConn, errors.New(" The command contains illegal characters.")) { + return + } + } + stdout, err := cmd.ExecWithCheck("docker", cmds...) + if wshandleError(wsConn, errors.WithMessage(err, stdout)) { + return + } + + commands := []string{"exec", "-it", containerID, command} + if len(user) != 0 { + commands = []string{"exec", "-it", "-u", user, containerID, command} + } + pidMap := loadMapFromDockerTop(containerID) + slave, err := terminal.NewCommand(commands) + if wshandleError(wsConn, err) { + return + } + defer killBash(containerID, command, pidMap) + defer slave.Close() + + tty, err := terminal.NewLocalWsSession(cols, rows, wsConn, slave, true) + if wshandleError(wsConn, err) { + return + } + + quitChan := make(chan bool, 3) + tty.Start(quitChan) + go slave.Wait(quitChan) + + <-quitChan + + global.LOG.Info("websocket finished") + if wshandleError(wsConn, err) { + return + } +} + +func wshandleError(ws *websocket.Conn, err error) bool { + if err != nil { + global.LOG.Errorf("handler ws faled:, err: %v", err) + dt := time.Now().Add(time.Second) + if ctlerr := ws.WriteControl(websocket.CloseMessage, []byte(err.Error()), dt); ctlerr != nil { + wsData, err := json.Marshal(terminal.WsMsg{ + Type: terminal.WsMsgCmd, + Data: base64.StdEncoding.EncodeToString([]byte(err.Error())), + }) + if err != nil { + _ = ws.WriteMessage(websocket.TextMessage, []byte("{\"type\":\"cmd\",\"data\":\"failed to encoding to json\"}")) + } else { + _ = ws.WriteMessage(websocket.TextMessage, wsData) + } + } + return true + } + return false +} + +func loadMapFromDockerTop(containerID string) map[string]string { + pidMap := make(map[string]string) + sudo := cmd.SudoHandleCmd() + + stdout, err := cmd.Execf("%s docker top %s -eo pid,command ", sudo, containerID) + if err != nil { + return pidMap + } + lines := strings.Split(stdout, "\n") + for _, line := range lines { + parts := strings.Fields(line) + if len(parts) < 2 { + continue + } + pidMap[parts[0]] = strings.Join(parts[1:], " ") + } + return pidMap +} + +func killBash(containerID, comm string, pidMap map[string]string) { + sudo := cmd.SudoHandleCmd() + newPidMap := loadMapFromDockerTop(containerID) + for pid, command := range newPidMap { + isOld := false + for pid2 := range pidMap { + if pid == pid2 { + isOld = true + break + } + } + if !isOld && command == comm { + _, _ = cmd.Execf("%s kill -9 %s", sudo, pid) + } + } +} + +var upGrader = websocket.Upgrader{ + ReadBufferSize: 1024, + WriteBufferSize: 1024 * 1024 * 10, + CheckOrigin: func(r *http.Request) bool { + return true + }, +} diff --git a/agent/app/api/v1/website.go b/agent/app/api/v1/website.go new file mode 100644 index 000000000..b5c6be084 --- /dev/null +++ b/agent/app/api/v1/website.go @@ -0,0 +1,822 @@ +package v1 + +import ( + "encoding/base64" + + "github.com/1Panel-dev/1Panel/agent/app/api/v1/helper" + "github.com/1Panel-dev/1Panel/agent/app/dto" + "github.com/1Panel-dev/1Panel/agent/app/dto/request" + "github.com/1Panel-dev/1Panel/agent/constant" + "github.com/gin-gonic/gin" +) + +// @Tags Website +// @Summary Page websites +// @Description 获取网站列表分页 +// @Accept json +// @Param request body request.WebsiteSearch true "request" +// @Success 200 {object} dto.PageResult +// @Security ApiKeyAuth +// @Router /websites/search [post] +func (b *BaseApi) PageWebsite(c *gin.Context) { + var req request.WebsiteSearch + if err := helper.CheckBindAndValidate(&req, c); err != nil { + return + } + total, websites, err := websiteService.PageWebsite(req) + if err != nil { + helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) + return + } + helper.SuccessWithData(c, dto.PageResult{ + Total: total, + Items: websites, + }) +} + +// @Tags Website +// @Summary List websites +// @Description 获取网站列表 +// @Success 200 {array} response.WebsiteDTO +// @Security ApiKeyAuth +// @Router /websites/list [get] +func (b *BaseApi) GetWebsites(c *gin.Context) { + websites, err := websiteService.GetWebsites() + if err != nil { + helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) + return + } + helper.SuccessWithData(c, websites) +} + +// @Tags Website +// @Summary List website names +// @Description 获取网站列表 +// @Success 200 {array} string +// @Security ApiKeyAuth +// @Router /websites/options [get] +func (b *BaseApi) GetWebsiteOptions(c *gin.Context) { + websites, err := websiteService.GetWebsiteOptions() + if err != nil { + helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) + return + } + helper.SuccessWithData(c, websites) +} + +// @Tags Website +// @Summary Create website +// @Description 创建网站 +// @Accept json +// @Param request body request.WebsiteCreate true "request" +// @Success 200 +// @Security ApiKeyAuth +// @Router /websites [post] +// @x-panel-log {"bodyKeys":["primaryDomain"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"创建网站 [primaryDomain]","formatEN":"Create website [primaryDomain]"} +func (b *BaseApi) CreateWebsite(c *gin.Context) { + var req request.WebsiteCreate + if err := helper.CheckBindAndValidate(&req, c); err != nil { + return + } + if len(req.FtpPassword) != 0 { + pass, err := base64.StdEncoding.DecodeString(req.FtpPassword) + if err != nil { + helper.ErrorWithDetail(c, constant.CodeErrBadRequest, constant.ErrTypeInvalidParams, err) + return + } + req.FtpPassword = string(pass) + } + err := websiteService.CreateWebsite(req) + if err != nil { + helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) + return + } + helper.SuccessWithData(c, nil) +} + +// @Tags Website +// @Summary Operate website +// @Description 操作网站 +// @Accept json +// @Param request body request.WebsiteOp true "request" +// @Success 200 +// @Security ApiKeyAuth +// @Router /websites/operate [post] +// @x-panel-log {"bodyKeys":["id", "operate"],"paramKeys":[],"BeforeFunctions":[{"input_column":"id","input_value":"id","isList":false,"db":"websites","output_column":"primary_domain","output_value":"domain"}],"formatZH":"[operate] 网站 [domain]","formatEN":"[operate] website [domain]"} +func (b *BaseApi) OpWebsite(c *gin.Context) { + var req request.WebsiteOp + if err := helper.CheckBindAndValidate(&req, c); err != nil { + return + } + err := websiteService.OpWebsite(req) + if err != nil { + helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) + return + } + helper.SuccessWithData(c, nil) +} + +// @Tags Website +// @Summary Delete website +// @Description 删除网站 +// @Accept json +// @Param request body request.WebsiteDelete true "request" +// @Success 200 +// @Security ApiKeyAuth +// @Router /websites/del [post] +// @x-panel-log {"bodyKeys":["id"],"paramKeys":[],"BeforeFunctions":[{"input_column":"id","input_value":"id","isList":false,"db":"websites","output_column":"primary_domain","output_value":"domain"}],"formatZH":"删除网站 [domain]","formatEN":"Delete website [domain]"} +func (b *BaseApi) DeleteWebsite(c *gin.Context) { + var req request.WebsiteDelete + if err := helper.CheckBindAndValidate(&req, c); err != nil { + return + } + err := websiteService.DeleteWebsite(req) + if err != nil { + helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) + return + } + helper.SuccessWithData(c, nil) +} + +// @Tags Website +// @Summary Update website +// @Description 更新网站 +// @Accept json +// @Param request body request.WebsiteUpdate true "request" +// @Success 200 +// @Security ApiKeyAuth +// @Router /websites/update [post] +// @x-panel-log {"bodyKeys":["primaryDomain"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"更新网站 [primaryDomain]","formatEN":"Update website [primaryDomain]"} +func (b *BaseApi) UpdateWebsite(c *gin.Context) { + var req request.WebsiteUpdate + if err := helper.CheckBindAndValidate(&req, c); err != nil { + return + } + if err := websiteService.UpdateWebsite(req); err != nil { + helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) + return + } + helper.SuccessWithData(c, nil) +} + +// @Tags Website +// @Summary Search website by id +// @Description 通过 id 查询网站 +// @Accept json +// @Param id path integer true "request" +// @Success 200 {object} response.WebsiteDTO +// @Security ApiKeyAuth +// @Router /websites/:id [get] +func (b *BaseApi) GetWebsite(c *gin.Context) { + id, err := helper.GetParamID(c) + if err != nil { + helper.ErrorWithDetail(c, constant.CodeErrBadRequest, constant.ErrTypeInternalServer, nil) + return + } + website, err := websiteService.GetWebsite(id) + if err != nil { + helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) + return + } + helper.SuccessWithData(c, website) +} + +// @Tags Website Nginx +// @Summary Search website nginx by id +// @Description 通过 id 查询网站 nginx +// @Accept json +// @Param id path integer true "request" +// @Success 200 {object} response.FileInfo +// @Security ApiKeyAuth +// @Router /websites/:id/config/:type [get] +func (b *BaseApi) GetWebsiteNginx(c *gin.Context) { + id, err := helper.GetParamID(c) + if err != nil { + helper.ErrorWithDetail(c, constant.CodeErrBadRequest, constant.ErrTypeInternalServer, nil) + return + } + configType := c.Param("type") + + fileInfo, err := websiteService.GetWebsiteNginxConfig(id, configType) + if err != nil { + helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) + return + } + helper.SuccessWithData(c, fileInfo) +} + +// @Tags Website Nginx +// @Summary Load nginx conf +// @Description 获取 nginx 配置 +// @Accept json +// @Param request body request.NginxScopeReq true "request" +// @Success 200 {object} response.WebsiteNginxConfig +// @Security ApiKeyAuth +// @Router /websites/config [post] +func (b *BaseApi) GetNginxConfig(c *gin.Context) { + var req request.NginxScopeReq + if err := helper.CheckBindAndValidate(&req, c); err != nil { + return + } + config, err := websiteService.GetNginxConfigByScope(req) + if err != nil { + helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) + return + } + helper.SuccessWithData(c, config) +} + +// @Tags Website Nginx +// @Summary Update nginx conf +// @Description 更新 nginx 配置 +// @Accept json +// @Param request body request.NginxConfigUpdate true "request" +// @Success 200 +// @Security ApiKeyAuth +// @Router /websites/config/update [post] +// @x-panel-log {"bodyKeys":["websiteId"],"paramKeys":[],"BeforeFunctions":[{"input_column":"id","input_value":"websiteId","isList":false,"db":"websites","output_column":"primary_domain","output_value":"domain"}],"formatZH":"nginx 配置修改 [domain]","formatEN":"Nginx conf update [domain]"} +func (b *BaseApi) UpdateNginxConfig(c *gin.Context) { + var req request.NginxConfigUpdate + if err := helper.CheckBindAndValidate(&req, c); err != nil { + return + } + if err := websiteService.UpdateNginxConfigByScope(req); err != nil { + helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) + return + } + helper.SuccessWithData(c, nil) +} + +// @Tags Website HTTPS +// @Summary Load https conf +// @Description 获取 https 配置 +// @Accept json +// @Param id path integer true "request" +// @Success 200 {object} response.WebsiteHTTPS +// @Security ApiKeyAuth +// @Router /websites/:id/https [get] +func (b *BaseApi) GetHTTPSConfig(c *gin.Context) { + id, err := helper.GetParamID(c) + if err != nil { + helper.ErrorWithDetail(c, constant.CodeErrBadRequest, constant.ErrTypeInternalServer, nil) + return + } + res, err := websiteService.GetWebsiteHTTPS(id) + if err != nil { + helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) + return + } + helper.SuccessWithData(c, res) +} + +// @Tags Website HTTPS +// @Summary Update https conf +// @Description 更新 https 配置 +// @Accept json +// @Param request body request.WebsiteHTTPSOp true "request" +// @Success 200 {object} response.WebsiteHTTPS +// @Security ApiKeyAuth +// @Router /websites/:id/https [post] +// @x-panel-log {"bodyKeys":["websiteId"],"paramKeys":[],"BeforeFunctions":[{"input_column":"id","input_value":"websiteId","isList":false,"db":"websites","output_column":"primary_domain","output_value":"domain"}],"formatZH":"更新网站 [domain] https 配置","formatEN":"Update website https [domain] conf"} +func (b *BaseApi) UpdateHTTPSConfig(c *gin.Context) { + var req request.WebsiteHTTPSOp + if err := helper.CheckBindAndValidate(&req, c); err != nil { + return + } + tx, ctx := helper.GetTxAndContext() + res, err := websiteService.OpWebsiteHTTPS(ctx, req) + if err != nil { + tx.Rollback() + helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) + return + } + tx.Commit() + helper.SuccessWithData(c, res) +} + +// @Tags Website +// @Summary Check before create website +// @Description 网站创建前检查 +// @Accept json +// @Param request body request.WebsiteInstallCheckReq true "request" +// @Success 200 {array} response.WebsitePreInstallCheck +// @Security ApiKeyAuth +// @Router /websites/check [post] +func (b *BaseApi) CreateWebsiteCheck(c *gin.Context) { + var req request.WebsiteInstallCheckReq + if err := helper.CheckBindAndValidate(&req, c); err != nil { + return + } + data, err := websiteService.PreInstallCheck(req) + if err != nil { + helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) + return + } + helper.SuccessWithData(c, data) +} + +// @Tags Website Nginx +// @Summary Update website nginx conf +// @Description 更新 网站 nginx 配置 +// @Accept json +// @Param request body request.WebsiteNginxUpdate true "request" +// @Success 200 +// @Security ApiKeyAuth +// @Router /websites/nginx/update [post] +// @x-panel-log {"bodyKeys":["id"],"paramKeys":[],"BeforeFunctions":[{"input_column":"id","input_value":"id","isList":false,"db":"websites","output_column":"primary_domain","output_value":"domain"}],"formatZH":"[domain] Nginx 配置修改","formatEN":"[domain] Nginx conf update"} +func (b *BaseApi) UpdateWebsiteNginxConfig(c *gin.Context) { + var req request.WebsiteNginxUpdate + if err := helper.CheckBindAndValidate(&req, c); err != nil { + return + } + if err := websiteService.UpdateNginxConfigFile(req); err != nil { + helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) + return + } + helper.SuccessWithData(c, nil) +} + +// @Tags Website +// @Summary Operate website log +// @Description 操作网站日志 +// @Accept json +// @Param request body request.WebsiteLogReq true "request" +// @Success 200 {object} response.WebsiteLog +// @Security ApiKeyAuth +// @Router /websites/log [post] +// @x-panel-log {"bodyKeys":["id", "operate"],"paramKeys":[],"BeforeFunctions":[{"input_column":"id","input_value":"id","isList":false,"db":"websites","output_column":"primary_domain","output_value":"domain"}],"formatZH":"[domain][operate] 日志","formatEN":"[domain][operate] logs"} +func (b *BaseApi) OpWebsiteLog(c *gin.Context) { + var req request.WebsiteLogReq + if err := helper.CheckBindAndValidate(&req, c); err != nil { + return + } + res, err := websiteService.OpWebsiteLog(req) + if err != nil { + helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) + return + } + helper.SuccessWithData(c, res) +} + +// @Tags Website +// @Summary Change default server +// @Description 操作网站日志 +// @Accept json +// @Param request body request.WebsiteDefaultUpdate true "request" +// @Success 200 +// @Security ApiKeyAuth +// @Router /websites/default/server [post] +// @x-panel-log {"bodyKeys":["id", "operate"],"paramKeys":[],"BeforeFunctions":[{"input_column":"id","input_value":"id","isList":false,"db":"websites","output_column":"primary_domain","output_value":"domain"}],"formatZH":"修改默认 server => [domain]","formatEN":"Change default server => [domain]"} +func (b *BaseApi) ChangeDefaultServer(c *gin.Context) { + var req request.WebsiteDefaultUpdate + if err := helper.CheckBindAndValidate(&req, c); err != nil { + return + } + if err := websiteService.ChangeDefaultServer(req.ID); err != nil { + helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) + return + } + helper.SuccessWithData(c, nil) +} + +// @Tags Website +// @Summary Load website php conf +// @Description 获取网站 php 配置 +// @Accept json +// @Param id path integer true "request" +// @Success 200 {object} response.PHPConfig +// @Security ApiKeyAuth +// @Router /websites/php/config/:id [get] +func (b *BaseApi) GetWebsitePHPConfig(c *gin.Context) { + id, err := helper.GetParamID(c) + if err != nil { + helper.ErrorWithDetail(c, constant.CodeErrBadRequest, constant.ErrTypeInternalServer, nil) + return + } + data, err := websiteService.GetPHPConfig(id) + if err != nil { + helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) + return + } + helper.SuccessWithData(c, data) +} + +// @Tags Website PHP +// @Summary Update website php conf +// @Description 更新 网站 PHP 配置 +// @Accept json +// @Param request body request.WebsitePHPConfigUpdate true "request" +// @Success 200 +// @Security ApiKeyAuth +// @Router /websites/php/config [post] +// @x-panel-log {"bodyKeys":["id"],"paramKeys":[],"BeforeFunctions":[{"input_column":"id","input_value":"id","isList":false,"db":"websites","output_column":"primary_domain","output_value":"domain"}],"formatZH":"[domain] PHP 配置修改","formatEN":"[domain] PHP conf update"} +func (b *BaseApi) UpdateWebsitePHPConfig(c *gin.Context) { + var req request.WebsitePHPConfigUpdate + if err := helper.CheckBindAndValidate(&req, c); err != nil { + return + } + if err := websiteService.UpdatePHPConfig(req); err != nil { + helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) + return + } + helper.SuccessWithData(c, nil) +} + +// @Tags Website PHP +// @Summary Update php conf +// @Description 更新 php 配置文件 +// @Accept json +// @Param request body request.WebsitePHPFileUpdate true "request" +// @Success 200 +// @Security ApiKeyAuth +// @Router /websites/php/update [post] +// @x-panel-log {"bodyKeys":["websiteId"],"paramKeys":[],"BeforeFunctions":[{"input_column":"id","input_value":"websiteId","isList":false,"db":"websites","output_column":"primary_domain","output_value":"domain"}],"formatZH":"php 配置修改 [domain]","formatEN":"Nginx conf update [domain]"} +func (b *BaseApi) UpdatePHPFile(c *gin.Context) { + var req request.WebsitePHPFileUpdate + if err := helper.CheckBindAndValidate(&req, c); err != nil { + return + } + if err := websiteService.UpdatePHPConfigFile(req); err != nil { + helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) + return + } + helper.SuccessWithData(c, nil) +} + +// @Tags Website PHP +// @Summary Update php version +// @Description 变更 php 版本 +// @Accept json +// @Param request body request.WebsitePHPVersionReq true "request" +// @Success 200 +// @Security ApiKeyAuth +// @Router /websites/php/version [post] +// @x-panel-log {"bodyKeys":["websiteId"],"paramKeys":[],"BeforeFunctions":[{"input_column":"id","input_value":"websiteId","isList":false,"db":"websites","output_column":"primary_domain","output_value":"domain"}],"formatZH":"php 版本变更 [domain]","formatEN":"php version update [domain]"} +func (b *BaseApi) ChangePHPVersion(c *gin.Context) { + var req request.WebsitePHPVersionReq + if err := helper.CheckBindAndValidate(&req, c); err != nil { + return + } + if err := websiteService.ChangePHPVersion(req); err != nil { + helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) + return + } + helper.SuccessWithOutData(c) +} + +// @Tags Website +// @Summary Get rewrite conf +// @Description 获取伪静态配置 +// @Accept json +// @Param request body request.NginxRewriteReq true "request" +// @Success 200 +// @Security ApiKeyAuth +// @Router /websites/rewrite [post] +func (b *BaseApi) GetRewriteConfig(c *gin.Context) { + var req request.NginxRewriteReq + if err := helper.CheckBindAndValidate(&req, c); err != nil { + return + } + res, err := websiteService.GetRewriteConfig(req) + if err != nil { + helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) + return + } + helper.SuccessWithData(c, res) +} + +// @Tags Website +// @Summary Update rewrite conf +// @Description 更新伪静态配置 +// @Accept json +// @Param request body request.NginxRewriteUpdate true "request" +// @Success 200 +// @Security ApiKeyAuth +// @Router /websites/rewrite/update [post] +// @x-panel-log {"bodyKeys":["websiteID"],"paramKeys":[],"BeforeFunctions":[{"input_column":"id","input_value":"websiteID","isList":false,"db":"websites","output_column":"primary_domain","output_value":"domain"}],"formatZH":"伪静态配置修改 [domain]","formatEN":"Nginx conf rewrite update [domain]"} +func (b *BaseApi) UpdateRewriteConfig(c *gin.Context) { + var req request.NginxRewriteUpdate + if err := helper.CheckBindAndValidate(&req, c); err != nil { + return + } + if err := websiteService.UpdateRewriteConfig(req); err != nil { + helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) + return + } + helper.SuccessWithData(c, nil) +} + +// @Tags Website +// @Summary Update Site Dir +// @Description 更新网站目录 +// @Accept json +// @Param request body request.WebsiteUpdateDir true "request" +// @Success 200 +// @Security ApiKeyAuth +// @Router /websites/dir/update [post] +// @x-panel-log {"bodyKeys":["id"],"paramKeys":[],"BeforeFunctions":[{"input_column":"id","input_value":"id","isList":false,"db":"websites","output_column":"primary_domain","output_value":"domain"}],"formatZH":"更新网站 [domain] 目录","formatEN":"Update domain [domain] dir"} +func (b *BaseApi) UpdateSiteDir(c *gin.Context) { + var req request.WebsiteUpdateDir + if err := helper.CheckBindAndValidate(&req, c); err != nil { + return + } + if err := websiteService.UpdateSiteDir(req); err != nil { + helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) + return + } + helper.SuccessWithOutData(c) +} + +// @Tags Website +// @Summary Update Site Dir permission +// @Description 更新网站目录权限 +// @Accept json +// @Param request body request.WebsiteUpdateDirPermission true "request" +// @Success 200 +// @Security ApiKeyAuth +// @Router /websites/dir/permission [post] +// @x-panel-log {"bodyKeys":["id"],"paramKeys":[],"BeforeFunctions":[{"input_column":"id","input_value":"id","isList":false,"db":"websites","output_column":"primary_domain","output_value":"domain"}],"formatZH":"更新网站 [domain] 目录权限","formatEN":"Update domain [domain] dir permission"} +func (b *BaseApi) UpdateSiteDirPermission(c *gin.Context) { + var req request.WebsiteUpdateDirPermission + if err := helper.CheckBindAndValidate(&req, c); err != nil { + return + } + if err := websiteService.UpdateSitePermission(req); err != nil { + helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) + return + } + helper.SuccessWithOutData(c) +} + +// @Tags Website +// @Summary Get proxy conf +// @Description 获取反向代理配置 +// @Accept json +// @Param request body request.WebsiteProxyReq true "request" +// @Success 200 +// @Security ApiKeyAuth +// @Router /websites/proxies [post] +func (b *BaseApi) GetProxyConfig(c *gin.Context) { + var req request.WebsiteProxyReq + if err := helper.CheckBindAndValidate(&req, c); err != nil { + return + } + res, err := websiteService.GetProxies(req.ID) + if err != nil { + helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) + return + } + helper.SuccessWithData(c, res) +} + +// @Tags Website +// @Summary Update proxy conf +// @Description 修改反向代理配置 +// @Accept json +// @Param request body request.WebsiteProxyConfig true "request" +// @Success 200 +// @Security ApiKeyAuth +// @Router /websites/proxies/update [post] +// @x-panel-log {"bodyKeys":["id"],"paramKeys":[],"BeforeFunctions":[{"input_column":"id","input_value":"id","isList":false,"db":"websites","output_column":"primary_domain","output_value":"domain"}],"formatZH":"修改网站 [domain] 反向代理配置 ","formatEN":"Update domain [domain] proxy config"} +func (b *BaseApi) UpdateProxyConfig(c *gin.Context) { + var req request.WebsiteProxyConfig + if err := helper.CheckBindAndValidate(&req, c); err != nil { + return + } + err := websiteService.OperateProxy(req) + if err != nil { + helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) + return + } + helper.SuccessWithOutData(c) +} + +// @Tags Website +// @Summary Update proxy file +// @Description 更新反向代理文件 +// @Accept json +// @Param request body request.NginxProxyUpdate true "request" +// @Success 200 +// @Security ApiKeyAuth +// @Router /websites/proxy/file [post] +// @x-panel-log {"bodyKeys":["websiteID"],"paramKeys":[],"BeforeFunctions":[{"input_column":"id","input_value":"websiteID","isList":false,"db":"websites","output_column":"primary_domain","output_value":"domain"}],"formatZH":"更新反向代理文件 [domain]","formatEN":"Nginx conf proxy file update [domain]"} +func (b *BaseApi) UpdateProxyConfigFile(c *gin.Context) { + var req request.NginxProxyUpdate + if err := helper.CheckBindAndValidate(&req, c); err != nil { + return + } + if err := websiteService.UpdateProxyFile(req); err != nil { + helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) + return + } + helper.SuccessWithOutData(c) +} + +// @Tags Website +// @Summary Get AuthBasic conf +// @Description 获取密码访问配置 +// @Accept json +// @Param request body request.NginxAuthReq true "request" +// @Success 200 +// @Security ApiKeyAuth +// @Router /websites/auths [post] +func (b *BaseApi) GetAuthConfig(c *gin.Context) { + var req request.NginxAuthReq + if err := helper.CheckBindAndValidate(&req, c); err != nil { + return + } + res, err := websiteService.GetAuthBasics(req) + if err != nil { + helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) + return + } + helper.SuccessWithData(c, res) +} + +// @Tags Website +// @Summary Get AuthBasic conf +// @Description 更新密码访问配置 +// @Accept json +// @Param request body request.NginxAuthUpdate true "request" +// @Success 200 +// @Security ApiKeyAuth +// @Router /websites/auths/update [post] +func (b *BaseApi) UpdateAuthConfig(c *gin.Context) { + var req request.NginxAuthUpdate + if err := helper.CheckBindAndValidate(&req, c); err != nil { + return + } + if err := websiteService.UpdateAuthBasic(req); err != nil { + helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) + return + } + helper.SuccessWithOutData(c) +} + +// @Tags Website +// @Summary Get AntiLeech conf +// @Description 获取防盗链配置 +// @Accept json +// @Param request body request.NginxCommonReq true "request" +// @Success 200 +// @Security ApiKeyAuth +// @Router /websites/leech [post] +func (b *BaseApi) GetAntiLeech(c *gin.Context) { + var req request.NginxCommonReq + if err := helper.CheckBindAndValidate(&req, c); err != nil { + return + } + res, err := websiteService.GetAntiLeech(req.WebsiteID) + if err != nil { + helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) + return + } + helper.SuccessWithData(c, res) +} + +// @Tags Website +// @Summary Update AntiLeech +// @Description 更新防盗链配置 +// @Accept json +// @Param request body request.NginxAntiLeechUpdate true "request" +// @Success 200 +// @Security ApiKeyAuth +// @Router /websites/leech/update [post] +func (b *BaseApi) UpdateAntiLeech(c *gin.Context) { + var req request.NginxAntiLeechUpdate + if err := helper.CheckBindAndValidate(&req, c); err != nil { + return + } + if err := websiteService.UpdateAntiLeech(req); err != nil { + helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) + return + } + helper.SuccessWithOutData(c) +} + +// @Tags Website +// @Summary Update redirect conf +// @Description 修改重定向配置 +// @Accept json +// @Param request body request.NginxRedirectReq true "request" +// @Success 200 +// @Security ApiKeyAuth +// @Router /websites/redirect/update [post] +// @x-panel-log {"bodyKeys":["websiteID"],"paramKeys":[],"BeforeFunctions":[{"input_column":"id","input_value":"websiteID","isList":false,"db":"websites","output_column":"primary_domain","output_value":"domain"}],"formatZH":"修改网站 [domain] 重定向理配置 ","formatEN":"Update domain [domain] redirect config"} +func (b *BaseApi) UpdateRedirectConfig(c *gin.Context) { + var req request.NginxRedirectReq + if err := helper.CheckBindAndValidate(&req, c); err != nil { + return + } + err := websiteService.OperateRedirect(req) + if err != nil { + helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) + return + } + helper.SuccessWithOutData(c) +} + +// @Tags Website +// @Summary Get redirect conf +// @Description 获取重定向配置 +// @Accept json +// @Param request body request.WebsiteProxyReq true "request" +// @Success 200 +// @Security ApiKeyAuth +// @Router /websites/redirect [post] +func (b *BaseApi) GetRedirectConfig(c *gin.Context) { + var req request.WebsiteRedirectReq + if err := helper.CheckBindAndValidate(&req, c); err != nil { + return + } + res, err := websiteService.GetRedirect(req.WebsiteID) + if err != nil { + helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) + return + } + helper.SuccessWithData(c, res) +} + +// @Tags Website +// @Summary Update redirect file +// @Description 更新重定向文件 +// @Accept json +// @Param request body request.NginxRedirectUpdate true "request" +// @Success 200 +// @Security ApiKeyAuth +// @Router /websites/redirect/file [post] +// @x-panel-log {"bodyKeys":["websiteID"],"paramKeys":[],"BeforeFunctions":[{"input_column":"id","input_value":"websiteID","isList":false,"db":"websites","output_column":"primary_domain","output_value":"domain"}],"formatZH":"更新重定向文件 [domain]","formatEN":"Nginx conf redirect file update [domain]"} +func (b *BaseApi) UpdateRedirectConfigFile(c *gin.Context) { + var req request.NginxRedirectUpdate + if err := helper.CheckBindAndValidate(&req, c); err != nil { + return + } + if err := websiteService.UpdateRedirectFile(req); err != nil { + helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) + return + } + helper.SuccessWithOutData(c) +} + +// @Tags Website +// @Summary Get website dir +// @Description 获取网站目录配置 +// @Accept json +// @Param request body request.WebsiteCommonReq true "request" +// @Success 200 +// @Security ApiKeyAuth +// @Router /websites/dir [post] +func (b *BaseApi) GetDirConfig(c *gin.Context) { + var req request.WebsiteCommonReq + if err := helper.CheckBindAndValidate(&req, c); err != nil { + return + } + res, err := websiteService.LoadWebsiteDirConfig(req) + if err != nil { + helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) + return + } + helper.SuccessWithData(c, res) +} + +// @Tags Website +// @Summary Get default html +// @Description 获取默认 html +// @Accept json +// @Success 200 {object} response.FileInfo +// @Security ApiKeyAuth +// @Router /websites/default/html/:type [get] +func (b *BaseApi) GetDefaultHtml(c *gin.Context) { + resourceType, err := helper.GetStrParamByKey(c, "type") + if err != nil { + helper.ErrorWithDetail(c, constant.CodeErrBadRequest, constant.ErrTypeInternalServer, nil) + return + } + fileInfo, err := websiteService.GetDefaultHtml(resourceType) + if err != nil { + helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) + return + } + helper.SuccessWithData(c, fileInfo) +} + +// @Tags Website +// @Summary Update default html +// @Description 更新默认 html +// @Accept json +// @Param request body request.WebsiteHtmlUpdate true "request" +// @Success 200 +// @Security ApiKeyAuth +// @Router /websites/default/html/update [post] +// @x-panel-log {"bodyKeys":["type"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"更新默认 html","formatEN":"Update default html"} +func (b *BaseApi) UpdateDefaultHtml(c *gin.Context) { + var req request.WebsiteHtmlUpdate + if err := helper.CheckBindAndValidate(&req, c); err != nil { + return + } + if err := websiteService.UpdateDefaultHtml(req); err != nil { + helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) + return + } + helper.SuccessWithOutData(c) +} diff --git a/agent/app/api/v1/website_acme_account.go b/agent/app/api/v1/website_acme_account.go new file mode 100644 index 000000000..5b08a9b2c --- /dev/null +++ b/agent/app/api/v1/website_acme_account.go @@ -0,0 +1,76 @@ +package v1 + +import ( + "github.com/1Panel-dev/1Panel/agent/app/api/v1/helper" + "github.com/1Panel-dev/1Panel/agent/app/dto" + "github.com/1Panel-dev/1Panel/agent/app/dto/request" + "github.com/1Panel-dev/1Panel/agent/constant" + "github.com/gin-gonic/gin" +) + +// @Tags Website Acme +// @Summary Page website acme accounts +// @Description 获取网站 acme 列表分页 +// @Accept json +// @Param request body dto.PageInfo true "request" +// @Success 200 {object} dto.PageResult +// @Security ApiKeyAuth +// @Router /websites/acme/search [post] +func (b *BaseApi) PageWebsiteAcmeAccount(c *gin.Context) { + var req dto.PageInfo + if err := helper.CheckBindAndValidate(&req, c); err != nil { + return + } + total, accounts, err := websiteAcmeAccountService.Page(req) + if err != nil { + helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) + return + } + helper.SuccessWithData(c, dto.PageResult{ + Total: total, + Items: accounts, + }) +} + +// @Tags Website Acme +// @Summary Create website acme account +// @Description 创建网站 acme +// @Accept json +// @Param request body request.WebsiteAcmeAccountCreate true "request" +// @Success 200 {object} response.WebsiteAcmeAccountDTO +// @Security ApiKeyAuth +// @Router /websites/acme [post] +// @x-panel-log {"bodyKeys":["email"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"创建网站 acme [email]","formatEN":"Create website acme [email]"} +func (b *BaseApi) CreateWebsiteAcmeAccount(c *gin.Context) { + var req request.WebsiteAcmeAccountCreate + if err := helper.CheckBindAndValidate(&req, c); err != nil { + return + } + res, err := websiteAcmeAccountService.Create(req) + if err != nil { + helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) + return + } + helper.SuccessWithData(c, res) +} + +// @Tags Website Acme +// @Summary Delete website acme account +// @Description 删除网站 acme +// @Accept json +// @Param request body request.WebsiteResourceReq true "request" +// @Success 200 +// @Security ApiKeyAuth +// @Router /websites/acme/del [post] +// @x-panel-log {"bodyKeys":["id"],"paramKeys":[],"BeforeFunctions":[{"input_column":"id","input_value":"id","isList":false,"db":"website_acme_accounts","output_column":"email","output_value":"email"}],"formatZH":"删除网站 acme [email]","formatEN":"Delete website acme [email]"} +func (b *BaseApi) DeleteWebsiteAcmeAccount(c *gin.Context) { + var req request.WebsiteResourceReq + if err := helper.CheckBindAndValidate(&req, c); err != nil { + return + } + if err := websiteAcmeAccountService.Delete(req.ID); err != nil { + helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) + return + } + helper.SuccessWithData(c, nil) +} diff --git a/agent/app/api/v1/website_ca.go b/agent/app/api/v1/website_ca.go new file mode 100644 index 000000000..1823944ea --- /dev/null +++ b/agent/app/api/v1/website_ca.go @@ -0,0 +1,178 @@ +package v1 + +import ( + "net/http" + "net/url" + "strconv" + + "github.com/1Panel-dev/1Panel/agent/app/api/v1/helper" + "github.com/1Panel-dev/1Panel/agent/app/dto" + "github.com/1Panel-dev/1Panel/agent/app/dto/request" + "github.com/1Panel-dev/1Panel/agent/constant" + "github.com/gin-gonic/gin" +) + +// @Tags Website CA +// @Summary Page website ca +// @Description 获取网站 ca 列表分页 +// @Accept json +// @Param request body request.WebsiteCASearch true "request" +// @Success 200 {object} dto.PageResult +// @Security ApiKeyAuth +// @Router /websites/ca/search [post] +func (b *BaseApi) PageWebsiteCA(c *gin.Context) { + var req request.WebsiteCASearch + if err := helper.CheckBindAndValidate(&req, c); err != nil { + return + } + total, cas, err := websiteCAService.Page(req) + if err != nil { + helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) + return + } + helper.SuccessWithData(c, dto.PageResult{ + Total: total, + Items: cas, + }) +} + +// @Tags Website CA +// @Summary Create website ca +// @Description 创建网站 ca +// @Accept json +// @Param request body request.WebsiteCACreate true "request" +// @Success 200 {object} request.WebsiteCACreate +// @Security ApiKeyAuth +// @Router /websites/ca [post] +// @x-panel-log {"bodyKeys":["name"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"创建网站 ca [name]","formatEN":"Create website ca [name]"} +func (b *BaseApi) CreateWebsiteCA(c *gin.Context) { + var req request.WebsiteCACreate + if err := helper.CheckBindAndValidate(&req, c); err != nil { + return + } + res, err := websiteCAService.Create(req) + if err != nil { + helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) + return + } + helper.SuccessWithData(c, res) +} + +// @Tags Website CA +// @Summary Get website ca +// @Description 获取网站 ca +// @Accept json +// @Param id path int true "id" +// @Success 200 {object} response.WebsiteCADTO +// @Security ApiKeyAuth +// @Router /websites/ca/{id} [get] +func (b *BaseApi) GetWebsiteCA(c *gin.Context) { + id, err := helper.GetParamID(c) + if err != nil { + return + } + res, err := websiteCAService.GetCA(id) + if err != nil { + helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) + return + } + helper.SuccessWithData(c, res) +} + +// @Tags Website CA +// @Summary Delete website ca +// @Description 删除网站 ca +// @Accept json +// @Param request body request.WebsiteCommonReq true "request" +// @Success 200 +// @Security ApiKeyAuth +// @Router /websites/ca/del [post] +// @x-panel-log {"bodyKeys":["id"],"paramKeys":[],"BeforeFunctions":[{"input_column":"id","input_value":"id","isList":false,"db":"website_cas","output_column":"name","output_value":"name"}],"formatZH":"删除网站 ca [name]","formatEN":"Delete website ca [name]"} +func (b *BaseApi) DeleteWebsiteCA(c *gin.Context) { + var req request.WebsiteCommonReq + if err := helper.CheckBindAndValidate(&req, c); err != nil { + return + } + if err := websiteCAService.Delete(req.ID); err != nil { + helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) + return + } + helper.SuccessWithOutData(c) +} + +// @Tags Website CA +// @Summary Obtain SSL +// @Description 自签 SSL 证书 +// @Accept json +// @Param request body request.WebsiteCAObtain true "request" +// @Success 200 +// @Security ApiKeyAuth +// @Router /websites/ca/obtain [post] +// @x-panel-log {"bodyKeys":["id"],"paramKeys":[],"BeforeFunctions":[{"input_column":"id","input_value":"id","isList":false,"db":"website_cas","output_column":"name","output_value":"name"}],"formatZH":"自签 SSL 证书 [name]","formatEN":"Obtain SSL [name]"} +func (b *BaseApi) ObtainWebsiteCA(c *gin.Context) { + var req request.WebsiteCAObtain + if err := helper.CheckBindAndValidate(&req, c); err != nil { + return + } + if _, err := websiteCAService.ObtainSSL(req); err != nil { + helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) + return + } + helper.SuccessWithOutData(c) +} + +// @Tags Website CA +// @Summary Obtain SSL +// @Description 续签 SSL 证书 +// @Accept json +// @Param request body request.WebsiteCAObtain true "request" +// @Success 200 +// @Security ApiKeyAuth +// @Router /websites/ca/renew [post] +// @x-panel-log {"bodyKeys":["id"],"paramKeys":[],"BeforeFunctions":[{"input_column":"id","input_value":"id","isList":false,"db":"website_cas","output_column":"name","output_value":"name"}],"formatZH":"自签 SSL 证书 [name]","formatEN":"Obtain SSL [name]"} +func (b *BaseApi) RenewWebsiteCA(c *gin.Context) { + var req request.WebsiteCARenew + if err := helper.CheckBindAndValidate(&req, c); err != nil { + return + } + if _, err := websiteCAService.ObtainSSL(request.WebsiteCAObtain{ + SSLID: req.SSLID, + Renew: true, + Unit: "year", + Time: 1, + }); err != nil { + helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) + return + } + helper.SuccessWithOutData(c) +} + +// @Tags Website CA +// @Summary Download CA file +// @Description 下载 CA 证书文件 +// @Accept json +// @Param request body request.WebsiteResourceReq true "request" +// @Success 200 +// @Security ApiKeyAuth +// @Router /websites/ca/download [post] +// @x-panel-log {"bodyKeys":["id"],"paramKeys":[],"BeforeFunctions":[{"input_column":"id","input_value":"id","isList":false,"db":"website_cas","output_column":"name","output_value":"name"}],"formatZH":"下载 CA 证书文件 [name]","formatEN":"download ca file [name]"} +func (b *BaseApi) DownloadCAFile(c *gin.Context) { + var req request.WebsiteResourceReq + if err := helper.CheckBindAndValidate(&req, c); err != nil { + return + } + file, err := websiteCAService.DownloadFile(req.ID) + if err != nil { + helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) + return + } + defer file.Close() + info, err := file.Stat() + if err != nil { + helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) + return + } + c.Header("Content-Length", strconv.FormatInt(info.Size(), 10)) + c.Header("Content-Disposition", "attachment; filename*=utf-8''"+url.PathEscape(info.Name())) + http.ServeContent(c.Writer, c.Request, info.Name(), info.ModTime(), file) +} diff --git a/agent/app/api/v1/website_dns_account.go b/agent/app/api/v1/website_dns_account.go new file mode 100644 index 000000000..9cc184681 --- /dev/null +++ b/agent/app/api/v1/website_dns_account.go @@ -0,0 +1,96 @@ +package v1 + +import ( + "github.com/1Panel-dev/1Panel/agent/app/api/v1/helper" + "github.com/1Panel-dev/1Panel/agent/app/dto" + "github.com/1Panel-dev/1Panel/agent/app/dto/request" + "github.com/1Panel-dev/1Panel/agent/constant" + "github.com/gin-gonic/gin" +) + +// @Tags Website DNS +// @Summary Page website dns accounts +// @Description 获取网站 dns 列表分页 +// @Accept json +// @Param request body dto.PageInfo true "request" +// @Success 200 {object} dto.PageResult +// @Security ApiKeyAuth +// @Router /websites/dns/search [post] +func (b *BaseApi) PageWebsiteDnsAccount(c *gin.Context) { + var req dto.PageInfo + if err := helper.CheckBindAndValidate(&req, c); err != nil { + return + } + total, accounts, err := websiteDnsAccountService.Page(req) + if err != nil { + helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) + return + } + helper.SuccessWithData(c, dto.PageResult{ + Total: total, + Items: accounts, + }) +} + +// @Tags Website DNS +// @Summary Create website dns account +// @Description 创建网站 dns +// @Accept json +// @Param request body request.WebsiteDnsAccountCreate true "request" +// @Success 200 +// @Security ApiKeyAuth +// @Router /websites/dns [post] +// @x-panel-log {"bodyKeys":["name"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"创建网站 dns [name]","formatEN":"Create website dns [name]"} +func (b *BaseApi) CreateWebsiteDnsAccount(c *gin.Context) { + var req request.WebsiteDnsAccountCreate + if err := helper.CheckBindAndValidate(&req, c); err != nil { + return + } + if _, err := websiteDnsAccountService.Create(req); err != nil { + helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) + return + } + helper.SuccessWithData(c, nil) +} + +// @Tags Website DNS +// @Summary Update website dns account +// @Description 更新网站 dns +// @Accept json +// @Param request body request.WebsiteDnsAccountUpdate true "request" +// @Success 200 +// @Security ApiKeyAuth +// @Router /websites/dns/update [post] +// @x-panel-log {"bodyKeys":["name"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"更新网站 dns [name]","formatEN":"Update website dns [name]"} +func (b *BaseApi) UpdateWebsiteDnsAccount(c *gin.Context) { + var req request.WebsiteDnsAccountUpdate + if err := helper.CheckBindAndValidate(&req, c); err != nil { + return + } + if _, err := websiteDnsAccountService.Update(req); err != nil { + helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) + return + } + helper.SuccessWithData(c, nil) +} + +// @Tags Website DNS +// @Summary Delete website dns account +// @Description 删除网站 dns +// @Accept json +// @Param request body request.WebsiteResourceReq true "request" +// @Success 200 +// @Security ApiKeyAuth +// @Router /websites/dns/del [post] +// @x-panel-log {"bodyKeys":["id"],"paramKeys":[],"BeforeFunctions":[{"input_column":"id","input_value":"id","isList":false,"db":"website_dns_accounts","output_column":"name","output_value":"name"}],"formatZH":"删除网站 dns [name]","formatEN":"Delete website dns [name]"} +func (b *BaseApi) DeleteWebsiteDnsAccount(c *gin.Context) { + var req request.WebsiteResourceReq + if err := helper.CheckBindAndValidate(&req, c); err != nil { + return + } + if err := websiteDnsAccountService.Delete(req.ID); err != nil { + helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) + return + } + helper.SuccessWithData(c, nil) +} diff --git a/agent/app/api/v1/website_domain.go b/agent/app/api/v1/website_domain.go new file mode 100644 index 000000000..6251e6043 --- /dev/null +++ b/agent/app/api/v1/website_domain.go @@ -0,0 +1,73 @@ +package v1 + +import ( + "github.com/1Panel-dev/1Panel/agent/app/api/v1/helper" + "github.com/1Panel-dev/1Panel/agent/app/dto/request" + "github.com/1Panel-dev/1Panel/agent/constant" + "github.com/gin-gonic/gin" +) + +// @Tags Website Domain +// @Summary Delete website domain +// @Description 删除网站域名 +// @Accept json +// @Param request body request.WebsiteDomainDelete true "request" +// @Success 200 +// @Security ApiKeyAuth +// @Router /websites/domains/del [post] +// @x-panel-log {"bodyKeys":["id"],"paramKeys":[],"BeforeFunctions":[{"input_column":"id","input_value":"id","isList":false,"db":"website_domains","output_column":"domain","output_value":"domain"}],"formatZH":"删除域名 [domain]","formatEN":"Delete domain [domain]"} +func (b *BaseApi) DeleteWebDomain(c *gin.Context) { + var req request.WebsiteDomainDelete + if err := helper.CheckBindAndValidate(&req, c); err != nil { + return + } + if err := websiteService.DeleteWebsiteDomain(req.ID); err != nil { + helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) + return + } + helper.SuccessWithData(c, nil) +} + +// @Tags Website Domain +// @Summary Create website domain +// @Description 创建网站域名 +// @Accept json +// @Param request body request.WebsiteDomainCreate true "request" +// @Success 200 {object} model.WebsiteDomain +// @Security ApiKeyAuth +// @Router /websites/domains [post] +// @x-panel-log {"bodyKeys":["domain"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"创建域名 [domain]","formatEN":"Create domain [domain]"} +func (b *BaseApi) CreateWebDomain(c *gin.Context) { + var req request.WebsiteDomainCreate + if err := helper.CheckBindAndValidate(&req, c); err != nil { + return + } + domain, err := websiteService.CreateWebsiteDomain(req) + if err != nil { + helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) + return + } + helper.SuccessWithData(c, domain) +} + +// @Tags Website Domain +// @Summary Search website domains by websiteId +// @Description 通过网站 id 查询域名 +// @Accept json +// @Param websiteId path integer true "request" +// @Success 200 {array} model.WebsiteDomain +// @Security ApiKeyAuth +// @Router /websites/domains/:websiteId [get] +func (b *BaseApi) GetWebDomains(c *gin.Context) { + websiteId, err := helper.GetIntParamByKey(c, "websiteId") + if err != nil { + helper.ErrorWithDetail(c, constant.CodeErrBadRequest, constant.ErrTypeInternalServer, nil) + return + } + list, err := websiteService.GetWebsiteDomain(websiteId) + if err != nil { + helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) + return + } + helper.SuccessWithData(c, list) +} diff --git a/agent/app/api/v1/website_ssl.go b/agent/app/api/v1/website_ssl.go new file mode 100644 index 000000000..e558d5fc1 --- /dev/null +++ b/agent/app/api/v1/website_ssl.go @@ -0,0 +1,248 @@ +package v1 + +import ( + "net/http" + "net/url" + "reflect" + "strconv" + + "github.com/1Panel-dev/1Panel/agent/app/api/v1/helper" + "github.com/1Panel-dev/1Panel/agent/app/dto" + "github.com/1Panel-dev/1Panel/agent/app/dto/request" + "github.com/1Panel-dev/1Panel/agent/constant" + "github.com/gin-gonic/gin" +) + +// @Tags Website SSL +// @Summary Page website ssl +// @Description 获取网站 ssl 列表分页 +// @Accept json +// @Param request body request.WebsiteSSLSearch true "request" +// @Success 200 +// @Security ApiKeyAuth +// @Router /websites/ssl/search [post] +func (b *BaseApi) PageWebsiteSSL(c *gin.Context) { + var req request.WebsiteSSLSearch + if err := helper.CheckBind(&req, c); err != nil { + return + } + if !reflect.DeepEqual(req.PageInfo, dto.PageInfo{}) { + total, accounts, err := websiteSSLService.Page(req) + if err != nil { + helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) + return + } + helper.SuccessWithData(c, dto.PageResult{ + Total: total, + Items: accounts, + }) + } else { + list, err := websiteSSLService.Search(req) + if err != nil { + helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) + return + } + helper.SuccessWithData(c, list) + } +} + +// @Tags Website SSL +// @Summary Create website ssl +// @Description 创建网站 ssl +// @Accept json +// @Param request body request.WebsiteSSLCreate true "request" +// @Success 200 {object} request.WebsiteSSLCreate +// @Security ApiKeyAuth +// @Router /websites/ssl [post] +// @x-panel-log {"bodyKeys":["primaryDomain"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"创建网站 ssl [primaryDomain]","formatEN":"Create website ssl [primaryDomain]"} +func (b *BaseApi) CreateWebsiteSSL(c *gin.Context) { + var req request.WebsiteSSLCreate + if err := helper.CheckBindAndValidate(&req, c); err != nil { + return + } + res, err := websiteSSLService.Create(req) + if err != nil { + helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) + return + } + helper.SuccessWithData(c, res) +} + +// @Tags Website SSL +// @Summary Apply ssl +// @Description 申请证书 +// @Accept json +// @Param request body request.WebsiteSSLApply true "request" +// @Success 200 +// @Security ApiKeyAuth +// @Router /websites/ssl/obtain [post] +// @x-panel-log {"bodyKeys":["ID"],"paramKeys":[],"BeforeFunctions":[{"input_column":"id","input_value":"ID","isList":false,"db":"website_ssls","output_column":"primary_domain","output_value":"domain"}],"formatZH":"申请证书 [domain]","formatEN":"apply ssl [domain]"} +func (b *BaseApi) ApplyWebsiteSSL(c *gin.Context) { + var req request.WebsiteSSLApply + if err := helper.CheckBindAndValidate(&req, c); err != nil { + return + } + if err := websiteSSLService.ObtainSSL(req); err != nil { + helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) + return + } + helper.SuccessWithOutData(c) +} + +// @Tags Website SSL +// @Summary Resolve website ssl +// @Description 解析网站 ssl +// @Accept json +// @Param request body request.WebsiteDNSReq true "request" +// @Success 200 {array} response.WebsiteDNSRes +// @Security ApiKeyAuth +// @Router /websites/ssl/resolve [post] +func (b *BaseApi) GetDNSResolve(c *gin.Context) { + var req request.WebsiteDNSReq + if err := helper.CheckBindAndValidate(&req, c); err != nil { + return + } + res, err := websiteSSLService.GetDNSResolve(req) + if err != nil { + helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) + return + } + helper.SuccessWithData(c, res) +} + +// @Tags Website SSL +// @Summary Delete website ssl +// @Description 删除网站 ssl +// @Accept json +// @Param request body request.WebsiteBatchDelReq true "request" +// @Success 200 +// @Security ApiKeyAuth +// @Router /websites/ssl/del [post] +// @x-panel-log {"bodyKeys":["ids"],"paramKeys":[],"BeforeFunctions":[{"input_column":"id","input_value":"ids","isList":true,"db":"website_ssls","output_column":"primary_domain","output_value":"domain"}],"formatZH":"删除 ssl [domain]","formatEN":"Delete ssl [domain]"} +func (b *BaseApi) DeleteWebsiteSSL(c *gin.Context) { + var req request.WebsiteBatchDelReq + if err := helper.CheckBindAndValidate(&req, c); err != nil { + return + } + if err := websiteSSLService.Delete(req.IDs); err != nil { + helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) + return + } + helper.SuccessWithData(c, nil) +} + +// @Tags Website SSL +// @Summary Search website ssl by website id +// @Description 通过网站 id 查询 ssl +// @Accept json +// @Param websiteId path integer true "request" +// @Success 200 +// @Security ApiKeyAuth +// @Router /websites/ssl/website/:websiteId [get] +func (b *BaseApi) GetWebsiteSSLByWebsiteId(c *gin.Context) { + websiteId, err := helper.GetIntParamByKey(c, "websiteId") + if err != nil { + helper.ErrorWithDetail(c, constant.CodeErrBadRequest, constant.ErrTypeInvalidParams, err) + return + } + websiteSSL, err := websiteSSLService.GetWebsiteSSL(websiteId) + if err != nil { + helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) + return + } + helper.SuccessWithData(c, websiteSSL) +} + +// @Tags Website SSL +// @Summary Search website ssl by id +// @Description 通过 id 查询 ssl +// @Accept json +// @Param id path integer true "request" +// @Success 200 +// @Security ApiKeyAuth +// @Router /websites/ssl/:id [get] +func (b *BaseApi) GetWebsiteSSLById(c *gin.Context) { + id, err := helper.GetParamID(c) + if err != nil { + helper.ErrorWithDetail(c, constant.CodeErrBadRequest, constant.ErrTypeInvalidParams, err) + return + } + websiteSSL, err := websiteSSLService.GetSSL(id) + if err != nil { + helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) + return + } + helper.SuccessWithData(c, websiteSSL) +} + +// @Tags Website SSL +// @Summary Update ssl +// @Description 更新 ssl +// @Accept json +// @Param request body request.WebsiteSSLUpdate true "request" +// @Success 200 +// @Security ApiKeyAuth +// @Router /websites/ssl/update [post] +// @x-panel-log {"bodyKeys":["id"],"paramKeys":[],"BeforeFunctions":[{"input_column":"id","input_value":"id","isList":false,"db":"website_ssls","output_column":"primary_domain","output_value":"domain"}],"formatZH":"更新证书设置 [domain]","formatEN":"Update ssl config [domain]"} +func (b *BaseApi) UpdateWebsiteSSL(c *gin.Context) { + var req request.WebsiteSSLUpdate + if err := helper.CheckBindAndValidate(&req, c); err != nil { + return + } + if err := websiteSSLService.Update(req); err != nil { + helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) + return + } + helper.SuccessWithData(c, nil) +} + +// @Tags Website SSL +// @Summary Upload ssl +// @Description 上传 ssl +// @Accept json +// @Param request body request.WebsiteSSLUpload true "request" +// @Success 200 +// @Security ApiKeyAuth +// @Router /websites/ssl/upload [post] +// @x-panel-log {"bodyKeys":["type"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"上传 ssl [type]","formatEN":"Upload ssl [type]"} +func (b *BaseApi) UploadWebsiteSSL(c *gin.Context) { + var req request.WebsiteSSLUpload + if err := helper.CheckBindAndValidate(&req, c); err != nil { + return + } + if err := websiteSSLService.Upload(req); err != nil { + helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) + return + } + helper.SuccessWithData(c, nil) +} + +// @Tags Website SSL +// @Summary Download SSL file +// @Description 下载证书文件 +// @Accept json +// @Param request body request.WebsiteResourceReq true "request" +// @Success 200 +// @Security ApiKeyAuth +// @Router /websites/ssl/download [post] +// @x-panel-log {"bodyKeys":["id"],"paramKeys":[],"BeforeFunctions":[{"input_column":"id","input_value":"id","isList":false,"db":"website_ssls","output_column":"primary_domain","output_value":"domain"}],"formatZH":"下载证书文件 [domain]","formatEN":"download ssl file [domain]"} +func (b *BaseApi) DownloadWebsiteSSL(c *gin.Context) { + var req request.WebsiteResourceReq + if err := helper.CheckBindAndValidate(&req, c); err != nil { + return + } + file, err := websiteSSLService.DownloadFile(req.ID) + if err != nil { + helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) + return + } + defer file.Close() + info, err := file.Stat() + if err != nil { + helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) + return + } + c.Header("Content-Length", strconv.FormatInt(info.Size(), 10)) + c.Header("Content-Disposition", "attachment; filename*=utf-8''"+url.PathEscape(info.Name())) + http.ServeContent(c.Writer, c.Request, info.Name(), info.ModTime(), file) +} diff --git a/agent/app/dto/app.go b/agent/app/dto/app.go new file mode 100644 index 000000000..d1655e439 --- /dev/null +++ b/agent/app/dto/app.go @@ -0,0 +1,149 @@ +package dto + +import ( + "github.com/1Panel-dev/1Panel/agent/app/model" +) + +type AppDatabase struct { + ServiceName string `json:"PANEL_DB_HOST"` + DbName string `json:"PANEL_DB_NAME"` + DbUser string `json:"PANEL_DB_USER"` + Password string `json:"PANEL_DB_USER_PASSWORD"` +} + +type AuthParam struct { + RootPassword string `json:"PANEL_DB_ROOT_PASSWORD"` + RootUser string `json:"PANEL_DB_ROOT_USER"` +} + +type RedisAuthParam struct { + RootPassword string `json:"PANEL_REDIS_ROOT_PASSWORD"` +} + +type MinioAuthParam struct { + RootPassword string `json:"PANEL_MINIO_ROOT_PASSWORD"` + RootUser string `json:"PANEL_MINIO_ROOT_USER"` +} + +type ContainerExec struct { + ContainerName string `json:"containerName"` + DbParam AppDatabase `json:"dbParam"` + Auth AuthParam `json:"auth"` +} + +type AppOssConfig struct { + Version string `json:"version"` + Package string `json:"package"` +} + +type AppVersion struct { + Version string `json:"version"` + DetailId uint `json:"detailId"` + DockerCompose string `json:"dockerCompose"` +} + +type AppList struct { + Valid bool `json:"valid"` + Violations []string `json:"violations"` + LastModified int `json:"lastModified"` + + Apps []AppDefine `json:"apps"` + Extra ExtraProperties `json:"additionalProperties"` +} + +type AppDefine struct { + Icon string `json:"icon"` + Name string `json:"name"` + ReadMe string `json:"readMe"` + LastModified int `json:"lastModified"` + + AppProperty AppProperty `json:"additionalProperties"` + Versions []AppConfigVersion `json:"versions"` +} + +type LocalAppAppDefine struct { + AppProperty model.App `json:"additionalProperties" yaml:"additionalProperties"` +} + +type LocalAppParam struct { + AppParams LocalAppInstallDefine `json:"additionalProperties" yaml:"additionalProperties"` +} + +type LocalAppInstallDefine struct { + FormFields interface{} `json:"formFields" yaml:"formFields"` +} + +type ExtraProperties struct { + Tags []Tag `json:"tags"` + Version string `json:"version"` +} + +type AppProperty struct { + Name string `json:"name"` + Type string `json:"type"` + Tags []string `json:"tags"` + ShortDescZh string `json:"shortDescZh"` + ShortDescEn string `json:"shortDescEn"` + Key string `json:"key"` + Required []string `json:"Required"` + CrossVersionUpdate bool `json:"crossVersionUpdate"` + Limit int `json:"limit"` + Recommend int `json:"recommend"` + Website string `json:"website"` + Github string `json:"github"` + Document string `json:"document"` +} + +type AppConfigVersion struct { + Name string `json:"name"` + LastModified int `json:"lastModified"` + DownloadUrl string `json:"downloadUrl"` + DownloadCallBackUrl string `json:"downloadCallBackUrl"` + AppForm interface{} `json:"additionalProperties"` +} + +type Tag struct { + Key string `json:"key"` + Name string `json:"name"` + Sort int `json:"sort"` +} + +type AppForm struct { + FormFields []AppFormFields `json:"formFields"` +} + +type AppFormFields struct { + Type string `json:"type"` + LabelZh string `json:"labelZh"` + LabelEn string `json:"labelEn"` + Required bool `json:"required"` + Default interface{} `json:"default"` + EnvKey string `json:"envKey"` + Disabled bool `json:"disabled"` + Edit bool `json:"edit"` + Rule string `json:"rule"` + Multiple bool `json:"multiple"` + Child interface{} `json:"child"` + Values []AppFormValue `json:"values"` +} + +type AppFormValue struct { + Label string `json:"label"` + Value string `json:"value"` +} + +type AppResource struct { + Type string `json:"type"` + Name string `json:"name"` +} + +var AppToolMap = map[string]string{ + "mysql": "phpmyadmin", + "redis": "redis-commander", +} + +type AppInstallInfo struct { + ID uint `json:"id"` + Key string `json:"key"` + Name string `json:"name"` +} diff --git a/agent/app/dto/backup.go b/agent/app/dto/backup.go new file mode 100644 index 000000000..971c06519 --- /dev/null +++ b/agent/app/dto/backup.go @@ -0,0 +1,82 @@ +package dto + +import "time" + +type BackupOperate struct { + ID uint `json:"id"` + Type string `json:"type" validate:"required"` + Bucket string `json:"bucket"` + AccessKey string `json:"accessKey"` + Credential string `json:"credential"` + BackupPath string `json:"backupPath"` + Vars string `json:"vars" validate:"required"` +} + +type BackupInfo struct { + ID uint `json:"id"` + CreatedAt time.Time `json:"createdAt"` + Type string `json:"type"` + Bucket string `json:"bucket"` + BackupPath string `json:"backupPath"` + Vars string `json:"vars"` +} + +type OneDriveInfo struct { + ClientID string `json:"client_id"` + ClientSecret string `json:"client_secret"` + RedirectUri string `json:"redirect_uri"` +} + +type BackupSearchFile struct { + Type string `json:"type" validate:"required"` +} + +type CommonBackup struct { + Type string `json:"type" validate:"required,oneof=app mysql mariadb redis website postgresql"` + Name string `json:"name"` + DetailName string `json:"detailName"` + Secret string `json:"secret"` +} +type CommonRecover struct { + Source string `json:"source" validate:"required,oneof=OSS S3 SFTP MINIO LOCAL COS KODO OneDrive WebDAV"` + Type string `json:"type" validate:"required,oneof=app mysql mariadb redis website postgresql"` + Name string `json:"name"` + DetailName string `json:"detailName"` + File string `json:"file"` + Secret string `json:"secret"` +} + +type RecordSearch struct { + PageInfo + Type string `json:"type" validate:"required"` + Name string `json:"name"` + DetailName string `json:"detailName"` +} + +type RecordSearchByCronjob struct { + PageInfo + CronjobID uint `json:"cronjobID" validate:"required"` +} + +type BackupRecords struct { + ID uint `json:"id"` + CreatedAt time.Time `json:"createdAt"` + Source string `json:"source"` + BackupType string `json:"backupType"` + FileDir string `json:"fileDir"` + FileName string `json:"fileName"` + Size int64 `json:"size"` +} + +type DownloadRecord struct { + Source string `json:"source" validate:"required,oneof=OSS S3 SFTP MINIO LOCAL COS KODO OneDrive WebDAV"` + FileDir string `json:"fileDir" validate:"required"` + FileName string `json:"fileName" validate:"required"` +} + +type ForBuckets struct { + Type string `json:"type" validate:"required"` + AccessKey string `json:"accessKey"` + Credential string `json:"credential" validate:"required"` + Vars string `json:"vars" validate:"required"` +} diff --git a/agent/app/dto/clam.go b/agent/app/dto/clam.go new file mode 100644 index 000000000..964db15ee --- /dev/null +++ b/agent/app/dto/clam.go @@ -0,0 +1,96 @@ +package dto + +import ( + "time" +) + +type SearchClamWithPage struct { + PageInfo + Info string `json:"info"` + OrderBy string `json:"orderBy" validate:"required,oneof=name status created_at"` + Order string `json:"order" validate:"required,oneof=null ascending descending"` +} + +type ClamBaseInfo struct { + Version string `json:"version"` + IsActive bool `json:"isActive"` + IsExist bool `json:"isExist"` + + FreshVersion string `json:"freshVersion"` + FreshIsActive bool `json:"freshIsActive"` + FreshIsExist bool `json:"freshIsExist"` +} + +type ClamInfo struct { + ID uint `json:"id"` + CreatedAt time.Time `json:"createdAt"` + + Name string `json:"name"` + Status string `json:"status"` + Path string `json:"path"` + InfectedStrategy string `json:"infectedStrategy"` + InfectedDir string `json:"infectedDir"` + LastHandleDate string `json:"lastHandleDate"` + Spec string `json:"spec"` + Description string `json:"description"` +} + +type ClamLogSearch struct { + PageInfo + + ClamID uint `json:"clamID"` + StartTime time.Time `json:"startTime"` + EndTime time.Time `json:"endTime"` +} + +type ClamLogReq struct { + Tail string `json:"tail"` + ClamName string `json:"clamName"` + RecordName string `json:"recordName"` +} + +type ClamFileReq struct { + Tail string `json:"tail"` + Name string `json:"name" validate:"required"` +} + +type ClamLog struct { + Name string `json:"name"` + ScanDate string `json:"scanDate"` + ScanTime string `json:"scanTime"` + InfectedFiles string `json:"infectedFiles"` + TotalError string `json:"totalError"` + Status string `json:"status"` +} + +type ClamCreate struct { + Name string `json:"name"` + Status string `json:"status"` + Path string `json:"path"` + InfectedStrategy string `json:"infectedStrategy"` + InfectedDir string `json:"infectedDir"` + Spec string `json:"spec"` + Description string `json:"description"` +} + +type ClamUpdate struct { + ID uint `json:"id"` + + Name string `json:"name"` + Path string `json:"path"` + InfectedStrategy string `json:"infectedStrategy"` + InfectedDir string `json:"infectedDir"` + Spec string `json:"spec"` + Description string `json:"description"` +} + +type ClamUpdateStatus struct { + ID uint `json:"id"` + Status string `json:"status"` +} + +type ClamDelete struct { + RemoveRecord bool `json:"removeRecord"` + RemoveInfected bool `json:"removeInfected"` + Ids []uint `json:"ids" validate:"required"` +} diff --git a/agent/app/dto/command.go b/agent/app/dto/command.go new file mode 100644 index 000000000..b085ce66f --- /dev/null +++ b/agent/app/dto/command.go @@ -0,0 +1,38 @@ +package dto + +type SearchCommandWithPage struct { + PageInfo + OrderBy string `json:"orderBy" validate:"required,oneof=name command created_at"` + Order string `json:"order" validate:"required,oneof=null ascending descending"` + GroupID uint `json:"groupID"` + Info string `json:"info"` + Name string `json:"name"` +} + +type CommandOperate struct { + ID uint `json:"id"` + GroupID uint `json:"groupID"` + GroupBelong string `json:"groupBelong"` + Name string `json:"name" validate:"required"` + Command string `json:"command" validate:"required"` +} + +type CommandInfo struct { + ID uint `json:"id"` + GroupID uint `json:"groupID"` + Name string `json:"name"` + Command string `json:"command"` + GroupBelong string `json:"groupBelong"` +} + +type CommandTree struct { + ID uint `json:"id"` + Label string `json:"label"` + Children []CommandInfo `json:"children"` +} + +type RedisCommand struct { + ID uint `json:"id"` + Name string `json:"name"` + Command string `json:"command"` +} diff --git a/agent/app/dto/common_req.go b/agent/app/dto/common_req.go new file mode 100644 index 000000000..b493fac55 --- /dev/null +++ b/agent/app/dto/common_req.go @@ -0,0 +1,54 @@ +package dto + +type SearchWithPage struct { + PageInfo + Info string `json:"info"` +} + +type PageInfo struct { + Page int `json:"page" validate:"required,number"` + PageSize int `json:"pageSize" validate:"required,number"` +} + +type UpdateDescription struct { + ID uint `json:"id" validate:"required"` + Description string `json:"description" validate:"max=256"` +} + +type OperationWithName struct { + Name string `json:"name" validate:"required"` +} + +type OperateByID struct { + ID uint `json:"id" validate:"required"` +} + +type Operate struct { + Operation string `json:"operation" validate:"required"` +} + +type BatchDeleteReq struct { + Ids []uint `json:"ids" validate:"required"` +} + +type FilePath struct { + Path string `json:"path" validate:"required"` +} + +type DeleteByName struct { + Name string `json:"name" validate:"required"` +} + +type UpdateByFile struct { + File string `json:"file"` +} + +type UpdateByNameAndFile struct { + Name string `json:"name"` + File string `json:"file"` +} + +type OperationWithNameAndType struct { + Name string `json:"name"` + Type string `json:"type" validate:"required"` +} diff --git a/agent/app/dto/common_res.go b/agent/app/dto/common_res.go new file mode 100644 index 000000000..5c7c11193 --- /dev/null +++ b/agent/app/dto/common_res.go @@ -0,0 +1,16 @@ +package dto + +type PageResult struct { + Total int64 `json:"total"` + Items interface{} `json:"items"` +} + +type Response struct { + Code int `json:"code"` + Message string `json:"message"` + Data interface{} `json:"data"` +} + +type Options struct { + Option string `json:"option"` +} diff --git a/agent/app/dto/compose_template.go b/agent/app/dto/compose_template.go new file mode 100644 index 000000000..de3832ab0 --- /dev/null +++ b/agent/app/dto/compose_template.go @@ -0,0 +1,23 @@ +package dto + +import "time" + +type ComposeTemplateCreate struct { + Name string `json:"name" validate:"required"` + Description string `json:"description"` + Content string `json:"content"` +} + +type ComposeTemplateUpdate struct { + ID uint `json:"id"` + Description string `json:"description"` + Content string `json:"content"` +} + +type ComposeTemplateInfo struct { + ID uint `json:"id"` + CreatedAt time.Time `json:"createdAt"` + Name string `json:"name"` + Description string `json:"description"` + Content string `json:"content"` +} diff --git a/agent/app/dto/container.go b/agent/app/dto/container.go new file mode 100644 index 000000000..90a351768 --- /dev/null +++ b/agent/app/dto/container.go @@ -0,0 +1,234 @@ +package dto + +import ( + "time" +) + +type PageContainer struct { + PageInfo + Name string `json:"name"` + State string `json:"state" validate:"required,oneof=all created running paused restarting removing exited dead"` + OrderBy string `json:"orderBy" validate:"required,oneof=name state created_at"` + Order string `json:"order" validate:"required,oneof=null ascending descending"` + Filters string `json:"filters"` + ExcludeAppStore bool `json:"excludeAppStore"` +} + +type InspectReq struct { + ID string `json:"id" validate:"required"` + Type string `json:"type" validate:"required"` +} + +type ContainerInfo struct { + ContainerID string `json:"containerID"` + Name string `json:"name"` + ImageId string `json:"imageID"` + ImageName string `json:"imageName"` + CreateTime string `json:"createTime"` + State string `json:"state"` + RunTime string `json:"runTime"` + + Network []string `json:"network"` + Ports []string `json:"ports"` + + IsFromApp bool `json:"isFromApp"` + IsFromCompose bool `json:"isFromCompose"` + + AppName string `json:"appName"` + AppInstallName string `json:"appInstallName"` + Websites []string `json:"websites"` +} + +type ResourceLimit struct { + CPU int `json:"cpu"` + Memory uint64 `json:"memory"` +} + +type ContainerOperate struct { + ContainerID string `json:"containerID"` + ForcePull bool `json:"forcePull"` + Name string `json:"name" validate:"required"` + Image string `json:"image" validate:"required"` + Network string `json:"network"` + Ipv4 string `json:"ipv4"` + Ipv6 string `json:"ipv6"` + PublishAllPorts bool `json:"publishAllPorts"` + ExposedPorts []PortHelper `json:"exposedPorts"` + Tty bool `json:"tty"` + OpenStdin bool `json:"openStdin"` + Cmd []string `json:"cmd"` + Entrypoint []string `json:"entrypoint"` + CPUShares int64 `json:"cpuShares"` + NanoCPUs float64 `json:"nanoCPUs"` + Memory float64 `json:"memory"` + Privileged bool `json:"privileged"` + AutoRemove bool `json:"autoRemove"` + Volumes []VolumeHelper `json:"volumes"` + Labels []string `json:"labels"` + Env []string `json:"env"` + RestartPolicy string `json:"restartPolicy"` +} + +type ContainerUpgrade struct { + Name string `json:"name" validate:"required"` + Image string `json:"image" validate:"required"` + ForcePull bool `json:"forcePull"` +} + +type ContainerListStats struct { + ContainerID string `json:"containerID"` + + CPUTotalUsage uint64 `json:"cpuTotalUsage"` + SystemUsage uint64 `json:"systemUsage"` + CPUPercent float64 `json:"cpuPercent"` + PercpuUsage int `json:"percpuUsage"` + + MemoryCache uint64 `json:"memoryCache"` + MemoryUsage uint64 `json:"memoryUsage"` + MemoryLimit uint64 `json:"memoryLimit"` + MemoryPercent float64 `json:"memoryPercent"` +} + +type ContainerStats struct { + CPUPercent float64 `json:"cpuPercent"` + Memory float64 `json:"memory"` + Cache float64 `json:"cache"` + IORead float64 `json:"ioRead"` + IOWrite float64 `json:"ioWrite"` + NetworkRX float64 `json:"networkRX"` + NetworkTX float64 `json:"networkTX"` + + ShotTime time.Time `json:"shotTime"` +} + +type VolumeHelper struct { + Type string `json:"type"` + SourceDir string `json:"sourceDir"` + ContainerDir string `json:"containerDir"` + Mode string `json:"mode"` +} +type PortHelper struct { + HostIP string `json:"hostIP"` + HostPort string `json:"hostPort"` + ContainerPort string `json:"containerPort"` + Protocol string `json:"protocol"` +} + +type ContainerOperation struct { + Names []string `json:"names" validate:"required"` + Operation string `json:"operation" validate:"required,oneof=start stop restart kill pause unpause remove"` +} + +type ContainerRename struct { + Name string `json:"name" validate:"required"` + NewName string `json:"newName" validate:"required"` +} + +type ContainerCommit struct { + ContainerId string `json:"containerID" validate:"required"` + ContainerName string `json:"containerName"` + NewImageName string `json:"newImageName"` + Comment string `json:"comment"` + Author string `json:"author"` + Pause bool `json:"pause"` +} + +type ContainerPrune struct { + PruneType string `json:"pruneType" validate:"required,oneof=container image volume network buildcache"` + WithTagAll bool `json:"withTagAll"` +} + +type ContainerPruneReport struct { + DeletedNumber int `json:"deletedNumber"` + SpaceReclaimed int `json:"spaceReclaimed"` +} + +type Network struct { + ID string `json:"id"` + Name string `json:"name"` + Labels []string `json:"labels"` + Driver string `json:"driver"` + IPAMDriver string `json:"ipamDriver"` + Subnet string `json:"subnet"` + Gateway string `json:"gateway"` + CreatedAt time.Time `json:"createdAt"` + Attachable bool `json:"attachable"` +} +type NetworkCreate struct { + Name string `json:"name" validate:"required"` + Driver string `json:"driver" validate:"required"` + Options []string `json:"options"` + Ipv4 bool `json:"ipv4"` + Subnet string `json:"subnet"` + Gateway string `json:"gateway"` + IPRange string `json:"ipRange"` + AuxAddress []SettingUpdate `json:"auxAddress"` + + Ipv6 bool `json:"ipv6"` + SubnetV6 string `json:"subnetV6"` + GatewayV6 string `json:"gatewayV6"` + IPRangeV6 string `json:"ipRangeV6"` + AuxAddressV6 []SettingUpdate `json:"auxAddressV6"` + Labels []string `json:"labels"` +} + +type Volume struct { + Name string `json:"name"` + Labels []string `json:"labels"` + Driver string `json:"driver"` + Mountpoint string `json:"mountpoint"` + CreatedAt time.Time `json:"createdAt"` +} +type VolumeCreate struct { + Name string `json:"name" validate:"required"` + Driver string `json:"driver" validate:"required"` + Options []string `json:"options"` + Labels []string `json:"labels"` +} + +type BatchDelete struct { + Force bool `json:"force"` + Names []string `json:"names" validate:"required"` +} + +type ComposeInfo struct { + Name string `json:"name"` + CreatedAt string `json:"createdAt"` + CreatedBy string `json:"createdBy"` + ContainerNumber int `json:"containerNumber"` + ConfigFile string `json:"configFile"` + Workdir string `json:"workdir"` + Path string `json:"path"` + Containers []ComposeContainer `json:"containers"` +} +type ComposeContainer struct { + ContainerID string `json:"containerID"` + Name string `json:"name"` + CreateTime string `json:"createTime"` + State string `json:"state"` +} +type ComposeCreate struct { + Name string `json:"name"` + From string `json:"from" validate:"required,oneof=edit path template"` + File string `json:"file"` + Path string `json:"path"` + Template uint `json:"template"` +} +type ComposeOperation struct { + Name string `json:"name" validate:"required"` + Path string `json:"path" validate:"required"` + Operation string `json:"operation" validate:"required,oneof=start stop down"` + WithFile bool `json:"withFile"` +} +type ComposeUpdate struct { + Name string `json:"name" validate:"required"` + Path string `json:"path" validate:"required"` + Content string `json:"content" validate:"required"` +} + +type ContainerLog struct { + Container string `json:"container" validate:"required"` + Since string `json:"since"` + Tail uint `json:"tail"` + ContainerType string `json:"containerType"` +} diff --git a/agent/app/dto/cronjob.go b/agent/app/dto/cronjob.go new file mode 100644 index 000000000..9189a7e09 --- /dev/null +++ b/agent/app/dto/cronjob.go @@ -0,0 +1,121 @@ +package dto + +import ( + "time" +) + +type PageCronjob struct { + PageInfo + Info string `json:"info"` + OrderBy string `json:"orderBy" validate:"required,oneof=name status created_at"` + Order string `json:"order" validate:"required,oneof=null ascending descending"` +} + +type CronjobCreate struct { + Name string `json:"name" validate:"required"` + Type string `json:"type" validate:"required"` + Spec string `json:"spec" validate:"required"` + + Script string `json:"script"` + Command string `json:"command"` + ContainerName string `json:"containerName"` + AppID string `json:"appID"` + Website string `json:"website"` + ExclusionRules string `json:"exclusionRules"` + DBType string `json:"dbType"` + DBName string `json:"dbName"` + URL string `json:"url"` + SourceDir string `json:"sourceDir"` + + BackupAccounts string `json:"backupAccounts"` + DefaultDownload string `json:"defaultDownload"` + RetainCopies int `json:"retainCopies" validate:"number,min=1"` + Secret string `json:"secret"` +} + +type CronjobUpdate struct { + ID uint `json:"id" validate:"required"` + Name string `json:"name" validate:"required"` + Spec string `json:"spec" validate:"required"` + + Script string `json:"script"` + Command string `json:"command"` + ContainerName string `json:"containerName"` + AppID string `json:"appID"` + Website string `json:"website"` + ExclusionRules string `json:"exclusionRules"` + DBType string `json:"dbType"` + DBName string `json:"dbName"` + URL string `json:"url"` + SourceDir string `json:"sourceDir"` + + BackupAccounts string `json:"backupAccounts"` + DefaultDownload string `json:"defaultDownload"` + RetainCopies int `json:"retainCopies" validate:"number,min=1"` + Secret string `json:"secret"` +} + +type CronjobUpdateStatus struct { + ID uint `json:"id" validate:"required"` + Status string `json:"status" validate:"required"` +} + +type CronjobDownload struct { + RecordID uint `json:"recordID" validate:"required"` + BackupAccountID uint `json:"backupAccountID" validate:"required"` +} + +type CronjobClean struct { + IsDelete bool `json:"isDelete"` + CleanData bool `json:"cleanData"` + CronjobID uint `json:"cronjobID" validate:"required"` +} + +type CronjobBatchDelete struct { + CleanData bool `json:"cleanData"` + IDs []uint `json:"ids" validate:"required"` +} + +type CronjobInfo struct { + ID uint `json:"id"` + Name string `json:"name"` + Type string `json:"type"` + Spec string `json:"spec"` + + Script string `json:"script"` + Command string `json:"command"` + ContainerName string `json:"containerName"` + AppID string `json:"appID"` + Website string `json:"website"` + ExclusionRules string `json:"exclusionRules"` + DBType string `json:"dbType"` + DBName string `json:"dbName"` + URL string `json:"url"` + SourceDir string `json:"sourceDir"` + BackupAccounts string `json:"backupAccounts"` + DefaultDownload string `json:"defaultDownload"` + RetainCopies int `json:"retainCopies"` + + LastRecordTime string `json:"lastRecordTime"` + Status string `json:"status"` + Secret string `json:"secret"` +} + +type SearchRecord struct { + PageInfo + CronjobID int `json:"cronjobID"` + StartTime time.Time `json:"startTime"` + EndTime time.Time `json:"endTime"` + Status string `json:"status"` +} + +type Record struct { + ID uint `json:"id"` + StartTime string `json:"startTime"` + Records string `json:"records"` + Status string `json:"status"` + Message string `json:"message"` + TargetPath string `json:"targetPath"` + Interval int `json:"interval"` + File string `json:"file"` +} diff --git a/agent/app/dto/dashboard.go b/agent/app/dto/dashboard.go new file mode 100644 index 000000000..a7d3a5c95 --- /dev/null +++ b/agent/app/dto/dashboard.go @@ -0,0 +1,107 @@ +package dto + +import "time" + +type DashboardBase struct { + WebsiteNumber int `json:"websiteNumber"` + DatabaseNumber int `json:"databaseNumber"` + CronjobNumber int `json:"cronjobNumber"` + AppInstalledNumber int `json:"appInstalledNumber"` + + Hostname string `json:"hostname"` + OS string `json:"os"` + Platform string `json:"platform"` + PlatformFamily string `json:"platformFamily"` + PlatformVersion string `json:"platformVersion"` + KernelArch string `json:"kernelArch"` + KernelVersion string `json:"kernelVersion"` + VirtualizationSystem string `json:"virtualizationSystem"` + + CPUCores int `json:"cpuCores"` + CPULogicalCores int `json:"cpuLogicalCores"` + CPUModelName string `json:"cpuModelName"` + + 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"` + + DiskSize int64 `json:"diskSize"` +} + +type DashboardCurrent struct { + Uptime uint64 `json:"uptime"` + TimeSinceUptime string `json:"timeSinceUptime"` + + Procs uint64 `json:"procs"` + + Load1 float64 `json:"load1"` + Load5 float64 `json:"load5"` + Load15 float64 `json:"load15"` + LoadUsagePercent float64 `json:"loadUsagePercent"` + + CPUPercent []float64 `json:"cpuPercent"` + CPUUsedPercent float64 `json:"cpuUsedPercent"` + CPUUsed float64 `json:"cpuUsed"` + CPUTotal int `json:"cpuTotal"` + + MemoryTotal uint64 `json:"memoryTotal"` + MemoryAvailable uint64 `json:"memoryAvailable"` + MemoryUsed uint64 `json:"memoryUsed"` + MemoryUsedPercent float64 `json:"memoryUsedPercent"` + + SwapMemoryTotal uint64 `json:"swapMemoryTotal"` + SwapMemoryAvailable uint64 `json:"swapMemoryAvailable"` + SwapMemoryUsed uint64 `json:"swapMemoryUsed"` + SwapMemoryUsedPercent float64 `json:"swapMemoryUsedPercent"` + + IOReadBytes uint64 `json:"ioReadBytes"` + IOWriteBytes uint64 `json:"ioWriteBytes"` + IOCount uint64 `json:"ioCount"` + IOReadTime uint64 `json:"ioReadTime"` + IOWriteTime uint64 `json:"ioWriteTime"` + + DiskData []DiskInfo `json:"diskData"` + + NetBytesSent uint64 `json:"netBytesSent"` + NetBytesRecv uint64 `json:"netBytesRecv"` + + GPUData []GPUInfo `json:"gpuData"` + + ShotTime time.Time `json:"shotTime"` +} + +type DiskInfo struct { + Path string `json:"path"` + Type string `json:"type"` + Device string `json:"device"` + Total uint64 `json:"total"` + Free uint64 `json:"free"` + Used uint64 `json:"used"` + UsedPercent float64 `json:"usedPercent"` + + InodesTotal uint64 `json:"inodesTotal"` + InodesUsed uint64 `json:"inodesUsed"` + InodesFree uint64 `json:"inodesFree"` + InodesUsedPercent float64 `json:"inodesUsedPercent"` +} + +type GPUInfo struct { + Index uint `json:"index"` + ProductName string `json:"productName"` + GPUUtil string `json:"gpuUtil"` + Temperature string `json:"temperature"` + PerformanceState string `json:"performanceState"` + PowerUsage string `json:"powerUsage"` + PowerDraw string `json:"powerDraw"` + MaxPowerLimit string `json:"maxPowerLimit"` + MemoryUsage string `json:"memoryUsage"` + MemUsed string `json:"memUsed"` + MemTotal string `json:"memTotal"` + FanSpeed string `json:"fanSpeed"` +} diff --git a/agent/app/dto/database.go b/agent/app/dto/database.go new file mode 100644 index 000000000..acfb3b09e --- /dev/null +++ b/agent/app/dto/database.go @@ -0,0 +1,321 @@ +package dto + +import "time" + +// common +type DBConfUpdateByFile struct { + Type string `json:"type" validate:"required,oneof=mysql mariadb postgresql redis"` + Database string `json:"database" validate:"required"` + File string `json:"file"` +} +type ChangeDBInfo struct { + ID uint `json:"id"` + From string `json:"from" validate:"required,oneof=local remote"` + Type string `json:"type" validate:"required,oneof=mysql mariadb postgresql"` + Database string `json:"database" validate:"required"` + Value string `json:"value" validate:"required"` +} + +type DBBaseInfo struct { + Name string `json:"name"` + ContainerName string `json:"containerName"` + Port int64 `json:"port"` +} + +// mysql +type MysqlDBSearch struct { + PageInfo + Info string `json:"info"` + Database string `json:"database" validate:"required"` + OrderBy string `json:"orderBy" validate:"required,oneof=name created_at"` + Order string `json:"order" validate:"required,oneof=null ascending descending"` +} + +type MysqlDBInfo struct { + ID uint `json:"id"` + CreatedAt time.Time `json:"createdAt"` + Name string `json:"name"` + From string `json:"from"` + MysqlName string `json:"mysqlName"` + Format string `json:"format"` + Username string `json:"username"` + Password string `json:"password"` + Permission string `json:"permission"` + IsDelete bool `json:"isDelete"` + Description string `json:"description"` +} + +type MysqlOption struct { + ID uint `json:"id"` + From string `json:"from"` + Type string `json:"type"` + Database string `json:"database"` + Name string `json:"name"` +} + +type MysqlDBCreate struct { + Name string `json:"name" validate:"required"` + From string `json:"from" validate:"required,oneof=local remote"` + Database string `json:"database" validate:"required"` + Format string `json:"format" validate:"required,oneof=utf8mb4 utf8 gbk big5"` + Username string `json:"username" validate:"required"` + Password string `json:"password" validate:"required"` + Permission string `json:"permission" validate:"required"` + Description string `json:"description"` +} + +type BindUser struct { + Database string `json:"database" validate:"required"` + DB string `json:"db" validate:"required"` + Username string `json:"username" validate:"required"` + Password string `json:"password" validate:"required"` + Permission string `json:"permission" validate:"required"` +} + +type MysqlLoadDB struct { + From string `json:"from" validate:"required,oneof=local remote"` + Type string `json:"type" validate:"required,oneof=mysql mariadb"` + Database string `json:"database" validate:"required"` +} + +type MysqlDBDeleteCheck struct { + ID uint `json:"id" validate:"required"` + Type string `json:"type" validate:"required,oneof=mysql mariadb"` + Database string `json:"database" validate:"required"` +} + +type MysqlDBDelete struct { + ID uint `json:"id" validate:"required"` + Type string `json:"type" validate:"required,oneof=mysql mariadb"` + Database string `json:"database" validate:"required"` + ForceDelete bool `json:"forceDelete"` + DeleteBackup bool `json:"deleteBackup"` +} + +type MysqlStatus struct { + AbortedClients string `json:"Aborted_clients"` + AbortedConnects string `json:"Aborted_connects"` + BytesReceived string `json:"Bytes_received"` + BytesSent string `json:"Bytes_sent"` + ComCommit string `json:"Com_commit"` + ComRollback string `json:"Com_rollback"` + Connections string `json:"Connections"` + CreatedTmpDiskTables string `json:"Created_tmp_disk_tables"` + CreatedTmpTables string `json:"Created_tmp_tables"` + InnodbBufferPoolPagesDirty string `json:"Innodb_buffer_pool_pages_dirty"` + InnodbBufferPoolReadRequests string `json:"Innodb_buffer_pool_read_requests"` + InnodbBufferPoolReads string `json:"Innodb_buffer_pool_reads"` + KeyReadRequests string `json:"Key_read_requests"` + KeyReads string `json:"Key_reads"` + KeyWriteEequests string `json:"Key_write_requests"` + KeyWrites string `json:"Key_writes"` + MaxUsedConnections string `json:"Max_used_connections"` + OpenTables string `json:"Open_tables"` + OpenedFiles string `json:"Opened_files"` + OpenedTables string `json:"Opened_tables"` + QcacheHits string `json:"Qcache_hits"` + QcacheInserts string `json:"Qcache_inserts"` + Questions string `json:"Questions"` + SelectFullJoin string `json:"Select_full_join"` + SelectRangeCheck string `json:"Select_range_check"` + SortMergePasses string `json:"Sort_merge_passes"` + TableLocksWaited string `json:"Table_locks_waited"` + ThreadsCached string `json:"Threads_cached"` + ThreadsConnected string `json:"Threads_connected"` + ThreadsCreated string `json:"Threads_created"` + ThreadsRunning string `json:"Threads_running"` + Uptime string `json:"Uptime"` + Run string `json:"Run"` + File string `json:"File"` + Position string `json:"Position"` +} + +type MysqlVariables struct { + BinlogCacheSize string `json:"binlog_cache_size"` + InnodbBufferPoolSize string `json:"innodb_buffer_pool_size"` + InnodbLogBufferSize string `json:"innodb_log_buffer_size"` + JoinBufferSize string `json:"join_buffer_size"` + KeyBufferSize string `json:"key_buffer_size"` + MaxConnections string `json:"max_connections"` + MaxHeapTableSize string `json:"max_heap_table_size"` + QueryCacheSize string `json:"query_cache_size"` + QueryCacheType string `json:"query_cache_type"` + ReadBufferSize string `json:"read_buffer_size"` + ReadRndBufferSize string `json:"read_rnd_buffer_size"` + SortBufferSize string `json:"sort_buffer_size"` + TableOpenCache string `json:"table_open_cache"` + ThreadCacheSize string `json:"thread_cache_size"` + ThreadStack string `json:"thread_stack"` + TmpTableSize string `json:"tmp_table_size"` + + SlowQueryLog string `json:"slow_query_log"` + LongQueryTime string `json:"long_query_time"` +} + +type MysqlVariablesUpdate struct { + Type string `json:"type" validate:"required,oneof=mysql mariadb"` + Database string `json:"database" validate:"required"` + Variables []MysqlVariablesUpdateHelper `json:"variables"` +} + +type MysqlVariablesUpdateHelper struct { + Param string `json:"param"` + Value interface{} `json:"value"` +} + +// redis +type ChangeRedisPass struct { + Database string `json:"database" validate:"required"` + Value string `json:"value"` +} + +type RedisConfUpdate struct { + Database string `json:"database" validate:"required"` + Timeout string `json:"timeout"` + Maxclients string `json:"maxclients"` + Maxmemory string `json:"maxmemory"` +} +type RedisConfPersistenceUpdate struct { + Database string `json:"database" validate:"required"` + Type string `json:"type" validate:"required,oneof=aof rbd"` + Appendonly string `json:"appendonly"` + Appendfsync string `json:"appendfsync"` + Save string `json:"save"` +} + +type RedisConf struct { + Database string `json:"database" validate:"required"` + Name string `json:"name"` + Port int64 `json:"port"` + ContainerName string `json:"containerName"` + Timeout string `json:"timeout"` + Maxclients string `json:"maxclients"` + Requirepass string `json:"requirepass"` + Maxmemory string `json:"maxmemory"` +} + +type RedisPersistence struct { + Database string `json:"database" validate:"required"` + Appendonly string `json:"appendonly"` + Appendfsync string `json:"appendfsync"` + Save string `json:"save"` +} + +type RedisStatus struct { + Database string `json:"database" validate:"required"` + TcpPort string `json:"tcp_port"` + UptimeInDays string `json:"uptime_in_days"` + ConnectedClients string `json:"connected_clients"` + UsedMemory string `json:"used_memory"` + UsedMemoryRss string `json:"used_memory_rss"` + UsedMemoryPeak string `json:"used_memory_peak"` + MemFragmentationRatio string `json:"mem_fragmentation_ratio"` + TotalConnectionsReceived string `json:"total_connections_received"` + TotalCommandsProcessed string `json:"total_commands_processed"` + InstantaneousOpsPerSec string `json:"instantaneous_ops_per_sec"` + KeyspaceHits string `json:"keyspace_hits"` + KeyspaceMisses string `json:"keyspace_misses"` + LatestForkUsec string `json:"latest_fork_usec"` +} + +type DatabaseFileRecords struct { + Database string `json:"database" validate:"required"` + FileName string `json:"fileName"` + FileDir string `json:"fileDir"` + CreatedAt string `json:"createdAt"` + Size int `json:"size"` +} +type RedisBackupRecover struct { + Database string `json:"database" validate:"required"` + FileName string `json:"fileName"` + FileDir string `json:"fileDir"` +} + +// database +type DatabaseSearch struct { + PageInfo + Info string `json:"info"` + Type string `json:"type"` + OrderBy string `json:"orderBy" validate:"required,oneof=name created_at"` + Order string `json:"order" validate:"required,oneof=null ascending descending"` +} + +type DatabaseInfo struct { + ID uint `json:"id"` + CreatedAt time.Time `json:"createdAt"` + Name string `json:"name" validate:"max=256"` + From string `json:"from"` + Type string `json:"type"` + Version string `json:"version"` + Address string `json:"address"` + Port uint `json:"port"` + Username string `json:"username"` + Password string `json:"password"` + + SSL bool `json:"ssl"` + RootCert string `json:"rootCert"` + ClientKey string `json:"clientKey"` + ClientCert string `json:"clientCert"` + SkipVerify bool `json:"skipVerify"` + + Description string `json:"description"` +} + +type DatabaseOption struct { + ID uint `json:"id"` + Type string `json:"type"` + From string `json:"from"` + Database string `json:"database"` + Version string `json:"version"` + Address string `json:"address"` +} + +type DatabaseItem struct { + ID uint `json:"id"` + From string `json:"from"` + Database string `json:"database"` + Name string `json:"name"` +} + +type DatabaseCreate struct { + Name string `json:"name" validate:"required,max=256"` + Type string `json:"type" validate:"required"` + From string `json:"from" validate:"required,oneof=local remote"` + Version string `json:"version" validate:"required"` + Address string `json:"address"` + Port uint `json:"port"` + Username string `json:"username" validate:"required"` + Password string `json:"password"` + + SSL bool `json:"ssl"` + RootCert string `json:"rootCert"` + ClientKey string `json:"clientKey"` + ClientCert string `json:"clientCert"` + SkipVerify bool `json:"skipVerify"` + + Description string `json:"description"` +} + +type DatabaseUpdate struct { + ID uint `json:"id"` + Type string `json:"type" validate:"required"` + Version string `json:"version" validate:"required"` + Address string `json:"address"` + Port uint `json:"port"` + Username string `json:"username" validate:"required"` + Password string `json:"password"` + + SSL bool `json:"ssl"` + RootCert string `json:"rootCert"` + ClientKey string `json:"clientKey"` + ClientCert string `json:"clientCert"` + SkipVerify bool `json:"skipVerify"` + + Description string `json:"description"` +} + +type DatabaseDelete struct { + ID uint `json:"id" validate:"required"` + ForceDelete bool `json:"forceDelete"` + DeleteBackup bool `json:"deleteBackup"` +} diff --git a/agent/app/dto/database_postgresql.go b/agent/app/dto/database_postgresql.go new file mode 100644 index 000000000..825ff3af5 --- /dev/null +++ b/agent/app/dto/database_postgresql.go @@ -0,0 +1,85 @@ +package dto + +import "time" + +type PostgresqlDBSearch struct { + PageInfo + Info string `json:"info"` + Database string `json:"database" validate:"required"` + OrderBy string `json:"orderBy" validate:"required,oneof=name created_at"` + Order string `json:"order" validate:"required,oneof=null ascending descending"` +} + +type PostgresqlDBInfo struct { + ID uint `json:"id"` + CreatedAt time.Time `json:"createdAt"` + Name string `json:"name"` + From string `json:"from"` + PostgresqlName string `json:"postgresqlName"` + Format string `json:"format"` + Username string `json:"username"` + Password string `json:"password"` + SuperUser bool `json:"superUser"` + IsDelete bool `json:"isDelete"` + Description string `json:"description"` +} + +type PostgresqlOption struct { + ID uint `json:"id"` + From string `json:"from"` + Type string `json:"type"` + Database string `json:"database"` + Name string `json:"name"` +} + +type PostgresqlDBCreate struct { + Name string `json:"name" validate:"required"` + From string `json:"from" validate:"required,oneof=local remote"` + Database string `json:"database" validate:"required"` + Format string `json:"format"` + Username string `json:"username" validate:"required"` + Password string `json:"password" validate:"required"` + SuperUser bool `json:"superUser"` + Description string `json:"description"` +} + +type PostgresqlBindUser struct { + Name string `json:"name" validate:"required"` + Database string `json:"database" validate:"required"` + Username string `json:"username" validate:"required"` + Password string `json:"password" validate:"required"` + SuperUser bool `json:"superUser"` +} + +type PostgresqlPrivileges struct { + Name string `json:"name" validate:"required"` + Database string `json:"database" validate:"required"` + Username string `json:"username" validate:"required"` + SuperUser bool `json:"superUser"` +} + +type PostgresqlLoadDB struct { + From string `json:"from" validate:"required,oneof=local remote"` + Type string `json:"type" validate:"required,oneof=postgresql"` + Database string `json:"database" validate:"required"` +} + +type PostgresqlDBDeleteCheck struct { + ID uint `json:"id" validate:"required"` + Type string `json:"type" validate:"required,oneof=postgresql"` + Database string `json:"database" validate:"required"` +} + +type PostgresqlDBDelete struct { + ID uint `json:"id" validate:"required"` + Type string `json:"type" validate:"required,oneof=postgresql"` + Database string `json:"database" validate:"required"` + ForceDelete bool `json:"forceDelete"` + DeleteBackup bool `json:"deleteBackup"` +} + +type PostgresqlConfUpdateByFile struct { + Type string `json:"type" validate:"required,oneof=postgresql mariadb"` + Database string `json:"database" validate:"required"` + File string `json:"file"` +} diff --git a/agent/app/dto/device.go b/agent/app/dto/device.go new file mode 100644 index 000000000..ca05cc5cb --- /dev/null +++ b/agent/app/dto/device.go @@ -0,0 +1,41 @@ +package dto + +type DeviceBaseInfo struct { + DNS []string `json:"dns"` + Hosts []HostHelper `json:"hosts"` + Hostname string `json:"hostname"` + TimeZone string `json:"timeZone"` + LocalTime string `json:"localTime"` + Ntp string `json:"ntp"` + User string `json:"user"` + + SwapMemoryTotal uint64 `json:"swapMemoryTotal"` + SwapMemoryAvailable uint64 `json:"swapMemoryAvailable"` + SwapMemoryUsed uint64 `json:"swapMemoryUsed"` + MaxSize uint64 `json:"maxSize"` + + SwapDetails []SwapHelper `json:"swapDetails"` +} + +type HostHelper struct { + IP string `json:"ip"` + Host string `json:"host"` +} + +type SwapHelper struct { + Path string `json:"path" validate:"required"` + Size uint64 `json:"size"` + Used string `json:"used"` + + IsNew bool `json:"isNew"` +} + +type TimeZoneOptions struct { + From string `json:"from"` + Zones []string `json:"zones"` +} + +type ChangePasswd struct { + User string `json:"user"` + Passwd string `json:"passwd"` +} diff --git a/agent/app/dto/docker.go b/agent/app/dto/docker.go new file mode 100644 index 000000000..bfabacc41 --- /dev/null +++ b/agent/app/dto/docker.go @@ -0,0 +1,39 @@ +package dto + +type DaemonJsonUpdateByFile struct { + File string `json:"file"` +} + +type DaemonJsonConf struct { + IsSwarm bool `json:"isSwarm"` + Status string `json:"status"` + Version string `json:"version"` + Mirrors []string `json:"registryMirrors"` + Registries []string `json:"insecureRegistries"` + LiveRestore bool `json:"liveRestore"` + IPTables bool `json:"iptables"` + CgroupDriver string `json:"cgroupDriver"` + + Ipv6 bool `json:"ipv6"` + FixedCidrV6 string `json:"fixedCidrV6"` + Ip6Tables bool `json:"ip6Tables"` + Experimental bool `json:"experimental"` + + LogMaxSize string `json:"logMaxSize"` + LogMaxFile string `json:"logMaxFile"` +} + +type LogOption struct { + LogMaxSize string `json:"logMaxSize"` + LogMaxFile string `json:"logMaxFile"` +} + +type Ipv6Option struct { + FixedCidrV6 string `json:"fixedCidrV6"` + Ip6Tables bool `json:"ip6Tables" validate:"required"` + Experimental bool `json:"experimental"` +} + +type DockerOperation struct { + Operation string `json:"operation" validate:"required,oneof=start restart stop"` +} diff --git a/agent/app/dto/fail2ban.go b/agent/app/dto/fail2ban.go new file mode 100644 index 000000000..8c8351b82 --- /dev/null +++ b/agent/app/dto/fail2ban.go @@ -0,0 +1,29 @@ +package dto + +type Fail2BanBaseInfo struct { + IsEnable bool `json:"isEnable"` + IsActive bool `json:"isActive"` + IsExist bool `json:"isExist"` + Version string `json:"version"` + + Port int `json:"port"` + MaxRetry int `json:"maxRetry"` + BanTime string `json:"banTime"` + FindTime string `json:"findTime"` + BanAction string `json:"banAction"` + LogPath string `json:"logPath"` +} + +type Fail2BanSearch struct { + Status string `json:"status" validate:"required,oneof=banned ignore"` +} + +type Fail2BanUpdate struct { + Key string `json:"key" validate:"required,oneof=port bantime findtime maxretry banaction logpath port"` + Value string `json:"value"` +} + +type Fail2BanSet struct { + IPs []string `json:"ips"` + Operate string `json:"operate" validate:"required,oneof=banned ignore"` +} diff --git a/agent/app/dto/firewall.go b/agent/app/dto/firewall.go new file mode 100644 index 000000000..c3d3646e1 --- /dev/null +++ b/agent/app/dto/firewall.go @@ -0,0 +1,74 @@ +package dto + +type FirewallBaseInfo struct { + Name string `json:"name"` + Status string `json:"status"` + Version string `json:"version"` + PingStatus string `json:"pingStatus"` +} + +type RuleSearch struct { + PageInfo + Info string `json:"info"` + Status string `json:"status"` + Strategy string `json:"strategy"` + Type string `json:"type" validate:"required"` +} + +type FirewallOperation struct { + Operation string `json:"operation" validate:"required,oneof=start stop restart disablePing enablePing"` +} + +type PortRuleOperate struct { + Operation string `json:"operation" validate:"required,oneof=add remove"` + Address string `json:"address"` + Port string `json:"port" validate:"required"` + Protocol string `json:"protocol" validate:"required,oneof=tcp udp tcp/udp"` + Strategy string `json:"strategy" validate:"required,oneof=accept drop"` + + Description string `json:"description"` +} + +type ForwardRuleOperate struct { + Rules []struct { + Operation string `json:"operation" validate:"required,oneof=add remove"` + Num string `json:"num"` + Protocol string `json:"protocol" validate:"required,oneof=tcp udp tcp/udp"` + Port string `json:"port" validate:"required"` + TargetIP string `json:"targetIP"` + TargetPort string `json:"targetPort" validate:"required"` + } `json:"rules"` +} + +type UpdateFirewallDescription struct { + Type string `json:"type"` + Address string `json:"address"` + Port string `json:"port"` + Protocol string `json:"protocol"` + Strategy string `json:"strategy" validate:"required,oneof=accept drop"` + + Description string `json:"description"` +} + +type AddrRuleOperate struct { + Operation string `json:"operation" validate:"required,oneof=add remove"` + Address string `json:"address" validate:"required"` + Strategy string `json:"strategy" validate:"required,oneof=accept drop"` + + Description string `json:"description"` +} + +type PortRuleUpdate struct { + OldRule PortRuleOperate `json:"oldRule"` + NewRule PortRuleOperate `json:"newRule"` +} + +type AddrRuleUpdate struct { + OldRule AddrRuleOperate `json:"oldRule"` + NewRule AddrRuleOperate `json:"newRule"` +} + +type BatchRuleOperate struct { + Type string `json:"type" validate:"required"` + Rules []PortRuleOperate `json:"rules"` +} diff --git a/agent/app/dto/ftp.go b/agent/app/dto/ftp.go new file mode 100644 index 000000000..fad9376d0 --- /dev/null +++ b/agent/app/dto/ftp.go @@ -0,0 +1,43 @@ +package dto + +import ( + "time" +) + +type FtpInfo struct { + ID uint `json:"id"` + CreatedAt time.Time `json:"createdAt"` + + User string `json:"user"` + Password string `json:"password"` + Path string `json:"path"` + Status string `json:"status"` + Description string `json:"description"` +} + +type FtpBaseInfo struct { + IsActive bool `json:"isActive"` + IsExist bool `json:"isExist"` +} + +type FtpLogSearch struct { + PageInfo + User string `json:"user"` + Operation string `json:"operation"` +} + +type FtpCreate struct { + User string `json:"user" validate:"required"` + Password string `json:"password" validate:"required"` + Path string `json:"path" validate:"required"` + Description string `json:"description"` +} + +type FtpUpdate struct { + ID uint `json:"id"` + + Password string `json:"password" validate:"required"` + Path string `json:"path" validate:"required"` + Status string `json:"status"` + Description string `json:"description"` +} diff --git a/agent/app/dto/group.go b/agent/app/dto/group.go new file mode 100644 index 000000000..60179d7ac --- /dev/null +++ b/agent/app/dto/group.go @@ -0,0 +1,25 @@ +package dto + +type GroupCreate struct { + ID uint `json:"id"` + Name string `json:"name" validate:"required"` + Type string `json:"type" validate:"required"` +} + +type GroupSearch struct { + Type string `json:"type" validate:"required"` +} + +type GroupUpdate struct { + ID uint `json:"id"` + Name string `json:"name"` + Type string `json:"type" validate:"required"` + IsDefault bool `json:"isDefault"` +} + +type GroupInfo struct { + ID uint `json:"id"` + Name string `json:"name"` + Type string `json:"type"` + IsDefault bool `json:"isDefault"` +} diff --git a/agent/app/dto/host.go b/agent/app/dto/host.go new file mode 100644 index 000000000..9853d8f34 --- /dev/null +++ b/agent/app/dto/host.go @@ -0,0 +1,75 @@ +package dto + +import ( + "time" +) + +type HostOperate struct { + ID uint `json:"id"` + GroupID uint `json:"groupID"` + Name string `json:"name"` + Addr string `json:"addr" validate:"required"` + Port uint `json:"port" validate:"required,number,max=65535,min=1"` + User string `json:"user" validate:"required"` + AuthMode string `json:"authMode" validate:"oneof=password key"` + Password string `json:"password"` + PrivateKey string `json:"privateKey"` + PassPhrase string `json:"passPhrase"` + RememberPassword bool `json:"rememberPassword"` + + Description string `json:"description"` +} + +type HostConnTest struct { + Addr string `json:"addr" validate:"required"` + Port uint `json:"port" validate:"required,number,max=65535,min=1"` + User string `json:"user" validate:"required"` + AuthMode string `json:"authMode" validate:"oneof=password key"` + Password string `json:"password"` + PrivateKey string `json:"privateKey"` + PassPhrase string `json:"passPhrase"` +} + +type SearchHostWithPage struct { + PageInfo + GroupID uint `json:"groupID"` + Info string `json:"info"` +} + +type SearchForTree struct { + Info string `json:"info"` +} + +type ChangeHostGroup struct { + ID uint `json:"id" validate:"required"` + GroupID uint `json:"groupID" validate:"required"` +} + +type HostInfo struct { + ID uint `json:"id"` + CreatedAt time.Time `json:"createdAt"` + GroupID uint `json:"groupID"` + GroupBelong string `json:"groupBelong"` + Name string `json:"name"` + Addr string `json:"addr"` + Port uint `json:"port"` + User string `json:"user"` + AuthMode string `json:"authMode"` + Password string `json:"password"` + PrivateKey string `json:"privateKey"` + PassPhrase string `json:"passPhrase"` + RememberPassword bool `json:"rememberPassword"` + + Description string `json:"description"` +} + +type HostTree struct { + ID uint `json:"id"` + Label string `json:"label"` + Children []TreeChild `json:"children"` +} + +type TreeChild struct { + ID uint `json:"id"` + Label string `json:"label"` +} diff --git a/agent/app/dto/image.go b/agent/app/dto/image.go new file mode 100644 index 000000000..541c5ffb0 --- /dev/null +++ b/agent/app/dto/image.go @@ -0,0 +1,44 @@ +package dto + +import "time" + +type ImageInfo struct { + ID string `json:"id"` + CreatedAt time.Time `json:"createdAt"` + IsUsed bool `json:"isUsed"` + Tags []string `json:"tags"` + Size string `json:"size"` +} + +type ImageLoad struct { + Path string `json:"path" validate:"required"` +} + +type ImageBuild struct { + From string `json:"from" validate:"required"` + Name string `json:"name" validate:"required"` + Dockerfile string `json:"dockerfile" validate:"required"` + Tags []string `json:"tags"` +} + +type ImagePull struct { + RepoID uint `json:"repoID"` + ImageName string `json:"imageName" validate:"required"` +} + +type ImageTag struct { + SourceID string `json:"sourceID" validate:"required"` + TargetName string `json:"targetName" validate:"required"` +} + +type ImagePush struct { + RepoID uint `json:"repoID" validate:"required"` + TagName string `json:"tagName" validate:"required"` + Name string `json:"name" validate:"required"` +} + +type ImageSave struct { + TagName string `json:"tagName" validate:"required"` + Path string `json:"path" validate:"required"` + Name string `json:"name" validate:"required"` +} diff --git a/agent/app/dto/image_repo.go b/agent/app/dto/image_repo.go new file mode 100644 index 000000000..ea5275106 --- /dev/null +++ b/agent/app/dto/image_repo.go @@ -0,0 +1,44 @@ +package dto + +import "time" + +type ImageRepoCreate struct { + Name string `json:"name" validate:"required"` + DownloadUrl string `json:"downloadUrl"` + Protocol string `json:"protocol"` + Username string `json:"username" validate:"max=256"` + Password string `json:"password" validate:"max=256"` + Auth bool `json:"auth"` +} + +type ImageRepoUpdate struct { + ID uint `json:"id"` + DownloadUrl string `json:"downloadUrl"` + Protocol string `json:"protocol"` + Username string `json:"username" validate:"max=256"` + Password string `json:"password" validate:"max=256"` + Auth bool `json:"auth"` +} + +type ImageRepoInfo struct { + ID uint `json:"id"` + CreatedAt time.Time `json:"createdAt"` + Name string `json:"name"` + DownloadUrl string `json:"downloadUrl"` + Protocol string `json:"protocol"` + Username string `json:"username"` + Auth bool `json:"auth"` + + Status string `json:"status"` + Message string `json:"message"` +} + +type ImageRepoOption struct { + ID uint `json:"id"` + Name string `json:"name"` + DownloadUrl string `json:"downloadUrl"` +} + +type ImageRepoDelete struct { + Ids []uint `json:"ids" validate:"required"` +} diff --git a/agent/app/dto/logs.go b/agent/app/dto/logs.go new file mode 100644 index 000000000..8831801db --- /dev/null +++ b/agent/app/dto/logs.go @@ -0,0 +1,50 @@ +package dto + +import ( + "time" +) + +type OperationLog struct { + ID uint `json:"id"` + Source string `json:"source"` + + IP string `json:"ip"` + Path string `json:"path"` + Method string `json:"method"` + UserAgent string `json:"userAgent"` + + Latency time.Duration `json:"latency"` + Status string `json:"status"` + Message string `json:"message"` + + DetailZH string `json:"detailZH"` + DetailEN string `json:"detailEN"` + CreatedAt time.Time `json:"createdAt"` +} + +type SearchOpLogWithPage struct { + PageInfo + Source string `json:"source"` + Status string `json:"status"` + Operation string `json:"operation"` +} + +type SearchLgLogWithPage struct { + PageInfo + IP string `json:"ip"` + Status string `json:"status"` +} + +type LoginLog struct { + ID uint `json:"id"` + IP string `json:"ip"` + Address string `json:"address"` + Agent string `json:"agent"` + Status string `json:"status"` + Message string `json:"message"` + CreatedAt time.Time `json:"createdAt"` +} + +type CleanLog struct { + LogType string `json:"logType" validate:"required,oneof=login operation"` +} diff --git a/agent/app/dto/monitor.go b/agent/app/dto/monitor.go new file mode 100644 index 000000000..780b1c790 --- /dev/null +++ b/agent/app/dto/monitor.go @@ -0,0 +1,16 @@ +package dto + +import "time" + +type MonitorSearch struct { + Param string `json:"param" validate:"required,oneof=all cpu memory load io network"` + Info string `json:"info"` + StartTime time.Time `json:"startTime"` + EndTime time.Time `json:"endTime"` +} + +type MonitorData struct { + Param string `json:"param" validate:"required,oneof=cpu memory load io network"` + Date []time.Time `json:"date"` + Value []interface{} `json:"value"` +} diff --git a/agent/app/dto/nginx.go b/agent/app/dto/nginx.go new file mode 100644 index 000000000..422c2d9f9 --- /dev/null +++ b/agent/app/dto/nginx.go @@ -0,0 +1,59 @@ +package dto + +import ( + "github.com/1Panel-dev/1Panel/agent/app/model" + "github.com/1Panel-dev/1Panel/agent/utils/nginx/components" +) + +type NginxFull struct { + Install model.AppInstall + Website model.Website + ConfigDir string + ConfigFile string + SiteDir string + Dir string + RootConfig NginxConfig + SiteConfig NginxConfig +} + +type NginxConfig struct { + FilePath string + Config *components.Config + OldContent string +} + +type NginxParam struct { + UpdateScope string + Name string + Params []string +} + +type NginxAuth struct { + Username string `json:"username"` + Remark string `json:"remark"` +} + +type NginxKey string + +const ( + Index NginxKey = "index" + LimitConn NginxKey = "limit-conn" + SSL NginxKey = "ssl" + CACHE NginxKey = "cache" + HttpPer NginxKey = "http-per" + ProxyCache NginxKey = "proxy-cache" +) + +var ScopeKeyMap = map[NginxKey][]string{ + Index: {"index"}, + LimitConn: {"limit_conn", "limit_rate", "limit_conn_zone"}, + SSL: {"ssl_certificate", "ssl_certificate_key"}, + HttpPer: {"server_names_hash_bucket_size", "client_header_buffer_size", "client_max_body_size", "keepalive_timeout", "gzip", "gzip_min_length", "gzip_comp_level"}, +} + +var StaticFileKeyMap = map[NginxKey]struct { +}{ + SSL: {}, + CACHE: {}, + ProxyCache: {}, +} diff --git a/agent/app/dto/request/app.go b/agent/app/dto/request/app.go new file mode 100644 index 000000000..8820231aa --- /dev/null +++ b/agent/app/dto/request/app.go @@ -0,0 +1,104 @@ +package request + +import ( + "github.com/1Panel-dev/1Panel/agent/app/dto" + "github.com/1Panel-dev/1Panel/agent/constant" +) + +type AppSearch struct { + dto.PageInfo + Name string `json:"name"` + Tags []string `json:"tags"` + Type string `json:"type"` + Recommend bool `json:"recommend"` + Resource string `json:"resource"` +} + +type AppInstallCreate struct { + AppDetailId uint `json:"appDetailId" validate:"required"` + Params map[string]interface{} `json:"params"` + Name string `json:"name" validate:"required"` + Services map[string]string `json:"services"` + AppContainerConfig +} + +type AppContainerConfig struct { + Advanced bool `json:"advanced"` + CpuQuota float64 `json:"cpuQuota"` + MemoryLimit float64 `json:"memoryLimit"` + MemoryUnit string `json:"memoryUnit"` + ContainerName string `json:"containerName"` + AllowPort bool `json:"allowPort"` + EditCompose bool `json:"editCompose"` + DockerCompose string `json:"dockerCompose"` + HostMode bool `json:"hostMode"` + PullImage bool `json:"pullImage"` +} + +type AppInstalledSearch struct { + dto.PageInfo + Type string `json:"type"` + Name string `json:"name"` + Tags []string `json:"tags"` + Update bool `json:"update"` + Unused bool `json:"unused"` + All bool `json:"all"` + Sync bool `json:"sync"` +} + +type AppInstalledInfo struct { + Key string `json:"key" validate:"required"` + Name string `json:"name"` +} + +type AppBackupSearch struct { + dto.PageInfo + AppInstallID uint `json:"appInstallID"` +} + +type AppBackupDelete struct { + Ids []uint `json:"ids"` +} + +type AppInstalledOperate struct { + InstallId uint `json:"installId" validate:"required"` + BackupId uint `json:"backupId"` + DetailId uint `json:"detailId"` + Operate constant.AppOperate `json:"operate" validate:"required"` + ForceDelete bool `json:"forceDelete"` + DeleteBackup bool `json:"deleteBackup"` + DeleteDB bool `json:"deleteDB"` + Backup bool `json:"backup"` + PullImage bool `json:"pullImage"` + DockerCompose string `json:"dockerCompose"` +} + +type AppInstallUpgrade struct { + InstallID uint `json:"installId"` + DetailID uint `json:"detailId"` + Backup bool `json:"backup"` + PullImage bool `json:"pullImage"` + DockerCompose string `json:"dockerCompose"` +} + +type AppInstalledUpdate struct { + InstallId uint `json:"installId" validate:"required"` + Params map[string]interface{} `json:"params" validate:"required"` + AppContainerConfig +} + +type AppInstalledIgnoreUpgrade struct { + DetailID uint `json:"detailID" validate:"required"` + Operate string `json:"operate" validate:"required,oneof=cancel ignore"` +} + +type PortUpdate struct { + Key string `json:"key"` + Name string `json:"name"` + Port int64 `json:"port"` +} + +type AppUpdateVersion struct { + AppInstallID uint `json:"appInstallID" validate:"required"` + UpdateVersion string `json:"updateVersion"` +} diff --git a/agent/app/dto/request/favorite.go b/agent/app/dto/request/favorite.go new file mode 100644 index 000000000..70280929c --- /dev/null +++ b/agent/app/dto/request/favorite.go @@ -0,0 +1,9 @@ +package request + +type FavoriteCreate struct { + Path string `json:"path" validate:"required"` +} + +type FavoriteDelete struct { + ID uint `json:"id" validate:"required"` +} diff --git a/agent/app/dto/request/file.go b/agent/app/dto/request/file.go new file mode 100644 index 000000000..ea31c8bc6 --- /dev/null +++ b/agent/app/dto/request/file.go @@ -0,0 +1,136 @@ +package request + +import ( + "github.com/1Panel-dev/1Panel/agent/app/dto" + "github.com/1Panel-dev/1Panel/agent/utils/files" +) + +type FileOption struct { + files.FileOption +} + +type FileContentReq struct { + Path string `json:"path" validate:"required"` + IsDetail bool `json:"isDetail"` +} + +type SearchUploadWithPage struct { + dto.PageInfo + Path string `json:"path" validate:"required"` +} + +type FileCreate struct { + Path string `json:"path" validate:"required"` + Content string `json:"content"` + IsDir bool `json:"isDir"` + Mode int64 `json:"mode"` + IsLink bool `json:"isLink"` + IsSymlink bool `json:"isSymlink"` + LinkPath string `json:"linkPath"` + Sub bool `json:"sub"` +} + +type FileRoleReq struct { + Paths []string `json:"paths" validate:"required"` + Mode int64 `json:"mode" validate:"required"` + User string `json:"user" validate:"required"` + Group string `json:"group" validate:"required"` + Sub bool `json:"sub"` +} + +type FileDelete struct { + Path string `json:"path" validate:"required"` + IsDir bool `json:"isDir"` + ForceDelete bool `json:"forceDelete"` +} + +type FileBatchDelete struct { + Paths []string `json:"paths" validate:"required"` + IsDir bool `json:"isDir"` +} + +type FileCompress struct { + Files []string `json:"files" validate:"required"` + Dst string `json:"dst" validate:"required"` + Type string `json:"type" validate:"required"` + Name string `json:"name" validate:"required"` + Replace bool `json:"replace"` + Secret string `json:"secret"` +} + +type FileDeCompress struct { + Dst string `json:"dst" validate:"required"` + Type string `json:"type" validate:"required"` + Path string `json:"path" validate:"required"` + Secret string `json:"secret"` +} + +type FileEdit struct { + Path string `json:"path" validate:"required"` + Content string `json:"content"` +} + +type FileRename struct { + OldName string `json:"oldName" validate:"required"` + NewName string `json:"newName" validate:"required"` +} + +type FilePathCheck struct { + Path string `json:"path" validate:"required"` +} + +type FileWget struct { + Url string `json:"url" validate:"required"` + Path string `json:"path" validate:"required"` + Name string `json:"name" validate:"required"` + IgnoreCertificate bool `json:"ignoreCertificate"` +} + +type FileMove struct { + Type string `json:"type" validate:"required"` + OldPaths []string `json:"oldPaths" validate:"required"` + NewPath string `json:"newPath" validate:"required"` + Name string `json:"name"` + Cover bool `json:"cover"` +} + +type FileDownload struct { + Paths []string `json:"paths" validate:"required"` + Type string `json:"type" validate:"required"` + Name string `json:"name" validate:"required"` + Compress bool `json:"compress"` +} + +type FileChunkDownload struct { + Path string `json:"path" validate:"required"` + Name string `json:"name" validate:"required"` +} + +type DirSizeReq struct { + Path string `json:"path" validate:"required"` +} + +type FileProcessReq struct { + Key string `json:"key"` +} + +type FileRoleUpdate struct { + Path string `json:"path" validate:"required"` + User string `json:"user" validate:"required"` + Group string `json:"group" validate:"required"` + Sub bool `json:"sub"` +} + +type FileReadByLineReq struct { + Page int `json:"page" validate:"required"` + PageSize int `json:"pageSize" validate:"required"` + Type string `json:"type" validate:"required"` + ID uint `json:"ID"` + Name string `json:"name"` + Latest bool `json:"latest"` +} + +type FileExistReq struct { + Name string `json:"name" validate:"required"` + Dir string `json:"dir" validate:"required"` +} diff --git a/agent/app/dto/request/host_tool.go b/agent/app/dto/request/host_tool.go new file mode 100644 index 000000000..aef43182d --- /dev/null +++ b/agent/app/dto/request/host_tool.go @@ -0,0 +1,41 @@ +package request + +type HostToolReq struct { + Type string `json:"type" validate:"required,oneof=supervisord"` + Operate string `json:"operate" validate:"oneof=status restart start stop"` +} + +type HostToolCreate struct { + Type string `json:"type" validate:"required"` + SupervisorConfig +} + +type SupervisorConfig struct { + ConfigPath string `json:"configPath"` + ServiceName string `json:"serviceName"` +} + +type HostToolLogReq struct { + Type string `json:"type" validate:"required,oneof=supervisord"` +} + +type HostToolConfig struct { + Type string `json:"type" validate:"required,oneof=supervisord"` + Operate string `json:"operate" validate:"oneof=get set"` + Content string `json:"content"` +} + +type SupervisorProcessConfig struct { + Name string `json:"name"` + Operate string `json:"operate"` + Command string `json:"command"` + User string `json:"user"` + Dir string `json:"dir"` + Numprocs string `json:"numprocs"` +} +type SupervisorProcessFileReq struct { + Name string `json:"name" validate:"required"` + Operate string `json:"operate" validate:"required,oneof=get clear update" ` + Content string `json:"content"` + File string `json:"file" validate:"required,oneof=out.log err.log config"` +} diff --git a/agent/app/dto/request/nginx.go b/agent/app/dto/request/nginx.go new file mode 100644 index 000000000..3cdc15ec4 --- /dev/null +++ b/agent/app/dto/request/nginx.go @@ -0,0 +1,87 @@ +package request + +import "github.com/1Panel-dev/1Panel/agent/app/dto" + +type NginxConfigFileUpdate struct { + Content string `json:"content" validate:"required"` + Backup bool `json:"backup"` +} + +type NginxScopeReq struct { + Scope dto.NginxKey `json:"scope" validate:"required"` + WebsiteID uint `json:"websiteId"` +} + +type NginxConfigUpdate struct { + Scope dto.NginxKey `json:"scope"` + Operate string `json:"operate" validate:"required,oneof=add update delete"` + WebsiteID uint `json:"websiteId"` + Params interface{} `json:"params"` +} + +type NginxRewriteReq struct { + WebsiteID uint `json:"websiteId" validate:"required"` + Name string `json:"name" validate:"required"` +} + +type NginxRewriteUpdate struct { + WebsiteID uint `json:"websiteId" validate:"required"` + Name string `json:"name" validate:"required"` + Content string `json:"content"` +} + +type NginxProxyUpdate struct { + WebsiteID uint `json:"websiteID" validate:"required"` + Content string `json:"content" validate:"required"` + Name string `json:"name" validate:"required"` +} + +type NginxAuthUpdate struct { + WebsiteID uint `json:"websiteID" validate:"required"` + Operate string `json:"operate" validate:"required"` + Username string `json:"username"` + Password string `json:"password"` + Remark string `json:"remark"` +} + +type NginxAuthReq struct { + WebsiteID uint `json:"websiteID" validate:"required"` +} + +type NginxCommonReq struct { + WebsiteID uint `json:"websiteID" validate:"required"` +} + +type NginxAntiLeechUpdate struct { + WebsiteID uint `json:"websiteID" validate:"required"` + Extends string `json:"extends" validate:"required"` + Return string `json:"return" validate:"required"` + Enable bool `json:"enable" ` + ServerNames []string `json:"serverNames"` + Cache bool `json:"cache"` + CacheTime int `json:"cacheTime"` + CacheUint string `json:"cacheUint"` + NoneRef bool `json:"noneRef"` + LogEnable bool `json:"logEnable"` + Blocked bool `json:"blocked"` +} + +type NginxRedirectReq struct { + Name string `json:"name" validate:"required"` + WebsiteID uint `json:"websiteID" validate:"required"` + Domains []string `json:"domains"` + KeepPath bool `json:"keepPath"` + Enable bool `json:"enable"` + Type string `json:"type" validate:"required"` + Redirect string `json:"redirect" validate:"required"` + Path string `json:"path"` + Target string `json:"target" validate:"required"` + Operate string `json:"operate" validate:"required"` + RedirectRoot bool `json:"redirectRoot"` +} + +type NginxRedirectUpdate struct { + WebsiteID uint `json:"websiteID" validate:"required"` + Content string `json:"content" validate:"required"` + Name string `json:"name" validate:"required"` +} diff --git a/agent/app/dto/request/php_extensions.go b/agent/app/dto/request/php_extensions.go new file mode 100644 index 000000000..2046f2b08 --- /dev/null +++ b/agent/app/dto/request/php_extensions.go @@ -0,0 +1,22 @@ +package request + +import "github.com/1Panel-dev/1Panel/agent/app/dto" + +type PHPExtensionsSearch struct { + dto.PageInfo + All bool `json:"all"` +} + +type PHPExtensionsCreate struct { + Name string `json:"name" validate:"required"` + Extensions string `json:"extensions" validate:"required"` +} + +type PHPExtensionsUpdate struct { + ID uint `json:"id" validate:"required"` + Extensions string `json:"extensions" validate:"required"` +} + +type PHPExtensionsDelete struct { + ID uint `json:"id" validate:"required"` +} diff --git a/agent/app/dto/request/process.go b/agent/app/dto/request/process.go new file mode 100644 index 000000000..9269c4c7d --- /dev/null +++ b/agent/app/dto/request/process.go @@ -0,0 +1,5 @@ +package request + +type ProcessReq struct { + PID int32 `json:"PID" validate:"required"` +} diff --git a/agent/app/dto/request/recycle_bin.go b/agent/app/dto/request/recycle_bin.go new file mode 100644 index 000000000..4870a735e --- /dev/null +++ b/agent/app/dto/request/recycle_bin.go @@ -0,0 +1,11 @@ +package request + +type RecycleBinCreate struct { + SourcePath string `json:"sourcePath" validate:"required"` +} + +type RecycleBinReduce struct { + From string `json:"from" validate:"required"` + RName string `json:"rName" validate:"required"` + Name string `json:"name"` +} diff --git a/agent/app/dto/request/runtime.go b/agent/app/dto/request/runtime.go new file mode 100644 index 000000000..b35bbdb6d --- /dev/null +++ b/agent/app/dto/request/runtime.go @@ -0,0 +1,72 @@ +package request + +import "github.com/1Panel-dev/1Panel/agent/app/dto" + +type RuntimeSearch struct { + dto.PageInfo + Type string `json:"type"` + Name string `json:"name"` + Status string `json:"status"` +} + +type RuntimeCreate struct { + AppDetailID uint `json:"appDetailId"` + Name string `json:"name"` + Params map[string]interface{} `json:"params"` + Resource string `json:"resource"` + Image string `json:"image"` + Type string `json:"type"` + Version string `json:"version"` + Source string `json:"source"` + CodeDir string `json:"codeDir"` + NodeConfig +} + +type NodeConfig struct { + Install bool `json:"install"` + Clean bool `json:"clean"` + Port int `json:"port"` + ExposedPorts []ExposedPort `json:"exposedPorts"` +} + +type ExposedPort struct { + HostPort int `json:"hostPort"` + ContainerPort int `json:"containerPort"` +} + +type RuntimeDelete struct { + ID uint `json:"id"` + ForceDelete bool `json:"forceDelete"` +} + +type RuntimeUpdate struct { + Name string `json:"name"` + ID uint `json:"id"` + Params map[string]interface{} `json:"params"` + Image string `json:"image"` + Version string `json:"version"` + Rebuild bool `json:"rebuild"` + Source string `json:"source"` + CodeDir string `json:"codeDir"` + NodeConfig +} + +type NodePackageReq struct { + CodeDir string `json:"codeDir"` +} + +type RuntimeOperate struct { + Operate string `json:"operate"` + ID uint `json:"ID"` +} + +type NodeModuleOperateReq struct { + Operate string `json:"operate" validate:"oneof=install uninstall update"` + ID uint `json:"ID" validate:"required"` + Module string `json:"module"` + PkgManager string `json:"pkgManager" validate:"oneof=npm yarn"` +} + +type NodeModuleReq struct { + ID uint `json:"ID" validate:"required"` +} diff --git a/agent/app/dto/request/website.go b/agent/app/dto/request/website.go new file mode 100644 index 000000000..839673375 --- /dev/null +++ b/agent/app/dto/request/website.go @@ -0,0 +1,220 @@ +package request + +import ( + "github.com/1Panel-dev/1Panel/agent/app/dto" +) + +type WebsiteSearch struct { + dto.PageInfo + Name string `json:"name"` + OrderBy string `json:"orderBy" validate:"required,oneof=primary_domain type status created_at expire_date"` + Order string `json:"order" validate:"required,oneof=null ascending descending"` + WebsiteGroupID uint `json:"websiteGroupId"` +} + +type WebsiteCreate struct { + PrimaryDomain string `json:"primaryDomain" validate:"required"` + Type string `json:"type" validate:"required"` + Alias string `json:"alias" validate:"required"` + Remark string `json:"remark"` + OtherDomains string `json:"otherDomains"` + Proxy string `json:"proxy"` + WebsiteGroupID uint `json:"webSiteGroupID" validate:"required"` + IPV6 bool `json:"IPV6"` + + AppType string `json:"appType" validate:"oneof=new installed"` + AppInstall NewAppInstall `json:"appInstall"` + AppID uint `json:"appID"` + AppInstallID uint `json:"appInstallID"` + + FtpUser string `json:"ftpUser"` + FtpPassword string `json:"ftpPassword"` + + RuntimeID uint `json:"runtimeID"` + RuntimeConfig +} + +type RuntimeConfig struct { + ProxyType string `json:"proxyType"` + Port int `json:"port"` +} + +type NewAppInstall struct { + Name string `json:"name"` + AppDetailId uint `json:"appDetailID"` + Params map[string]interface{} `json:"params"` + + AppContainerConfig +} + +type WebsiteInstallCheckReq struct { + InstallIds []uint `json:"InstallIds"` +} + +type WebsiteUpdate struct { + ID uint `json:"id" validate:"required"` + PrimaryDomain string `json:"primaryDomain" validate:"required"` + Remark string `json:"remark"` + WebsiteGroupID uint `json:"webSiteGroupID"` + ExpireDate string `json:"expireDate"` + IPV6 bool `json:"IPV6"` +} + +type WebsiteDelete struct { + ID uint `json:"id" validate:"required"` + DeleteApp bool `json:"deleteApp"` + DeleteBackup bool `json:"deleteBackup"` + ForceDelete bool `json:"forceDelete"` +} + +type WebsiteOp struct { + ID uint `json:"id" validate:"required"` + Operate string `json:"operate"` +} + +type WebsiteRedirectUpdate struct { + WebsiteID uint `json:"websiteId" validate:"required"` + Key string `json:"key" validate:"required"` + Enable bool `json:"enable"` +} + +type WebsiteRecover struct { + WebsiteName string `json:"websiteName" validate:"required"` + Type string `json:"type" validate:"required"` + BackupName string `json:"backupName" validate:"required"` +} + +type WebsiteRecoverByFile struct { + WebsiteName string `json:"websiteName" validate:"required"` + Type string `json:"type" validate:"required"` + FileDir string `json:"fileDir" validate:"required"` + FileName string `json:"fileName" validate:"required"` +} + +type WebsiteGroupCreate struct { + Name string `json:"name" validate:"required"` +} + +type WebsiteGroupUpdate struct { + ID uint `json:"id" validate:"required"` + Name string `json:"name"` + Default bool `json:"default"` +} + +type WebsiteDomainCreate struct { + WebsiteID uint `json:"websiteID" validate:"required"` + Domains string `json:"domains" validate:"required"` +} + +type WebsiteDomainDelete struct { + ID uint `json:"id" validate:"required"` +} + +type WebsiteHTTPSOp struct { + WebsiteID uint `json:"websiteId" validate:"required"` + Enable bool `json:"enable"` + WebsiteSSLID uint `json:"websiteSSLId"` + Type string `json:"type" validate:"oneof=existed auto manual"` + PrivateKey string `json:"privateKey"` + Certificate string `json:"certificate"` + PrivateKeyPath string `json:"privateKeyPath"` + CertificatePath string `json:"certificatePath"` + ImportType string `json:"importType"` + HttpConfig string `json:"httpConfig" validate:"oneof=HTTPSOnly HTTPAlso HTTPToHTTPS"` + SSLProtocol []string `json:"SSLProtocol"` + Algorithm string `json:"algorithm"` + Hsts bool `json:"hsts"` +} + +type WebsiteNginxUpdate struct { + ID uint `json:"id" validate:"required"` + Content string `json:"content" validate:"required"` +} + +type WebsiteLogReq struct { + ID uint `json:"id" validate:"required"` + Operate string `json:"operate" validate:"required"` + LogType string `json:"logType" validate:"required"` + Page int `json:"page"` + PageSize int `json:"pageSize"` +} + +type WebsiteDefaultUpdate struct { + ID uint `json:"id"` +} + +type WebsitePHPConfigUpdate struct { + ID uint `json:"id" validate:"required"` + Params map[string]string `json:"params"` + Scope string `json:"scope" validate:"required"` + DisableFunctions []string `json:"disableFunctions"` + UploadMaxSize string `json:"uploadMaxSize"` +} + +type WebsitePHPFileUpdate struct { + ID uint `json:"id" validate:"required"` + Type string `json:"type" validate:"required"` + Content string `json:"content" validate:"required"` +} + +type WebsitePHPVersionReq struct { + WebsiteID uint `json:"websiteID" validate:"required"` + RuntimeID uint `json:"runtimeID" validate:"required"` + RetainConfig bool `json:"retainConfig" ` +} + +type WebsiteUpdateDir struct { + ID uint `json:"id" validate:"required"` + SiteDir string `json:"siteDir" validate:"required"` +} + +type WebsiteUpdateDirPermission struct { + ID uint `json:"id" validate:"required"` + User string `json:"user" validate:"required"` + Group string `json:"group" validate:"required"` +} + +type WebsiteProxyConfig struct { + ID uint `json:"id" validate:"required"` + Operate string `json:"operate" validate:"required"` + Enable bool `json:"enable" ` + Cache bool `json:"cache" ` + CacheTime int `json:"cacheTime" ` + CacheUnit string `json:"cacheUnit"` + Name string `json:"name" validate:"required"` + Modifier string `json:"modifier"` + Match string `json:"match" validate:"required"` + ProxyPass string `json:"proxyPass" validate:"required"` + ProxyHost string `json:"proxyHost" validate:"required"` + Content string `json:"content"` + FilePath string `json:"filePath"` + Replaces map[string]string `json:"replaces"` + SNI bool `json:"sni"` +} + +type WebsiteProxyReq struct { + ID uint `json:"id" validate:"required"` +} + +type WebsiteRedirectReq struct { + WebsiteID uint `json:"websiteId" validate:"required"` +} + +type WebsiteCommonReq struct { + ID uint `json:"id" validate:"required"` +} + +type WafWebsite struct { + Key string `json:"key"` + Domains []string `json:"domains"` + Host []string `json:"host"` +} + +type WebsiteHtmlReq struct { + Type string `json:"type" validate:"required"` +} + +type WebsiteHtmlUpdate struct { + Type string `json:"type" validate:"required"` + Content string `json:"content" validate:"required"` +} diff --git a/agent/app/dto/request/website_ssl.go b/agent/app/dto/request/website_ssl.go new file mode 100644 index 000000000..851a57c2a --- /dev/null +++ b/agent/app/dto/request/website_ssl.go @@ -0,0 +1,139 @@ +package request + +import "github.com/1Panel-dev/1Panel/agent/app/dto" + +type WebsiteSSLSearch struct { + dto.PageInfo + AcmeAccountID string `json:"acmeAccountID"` +} + +type WebsiteSSLCreate struct { + PrimaryDomain string `json:"primaryDomain" validate:"required"` + OtherDomains string `json:"otherDomains"` + Provider string `json:"provider" validate:"required"` + AcmeAccountID uint `json:"acmeAccountId" validate:"required"` + DnsAccountID uint `json:"dnsAccountId"` + AutoRenew bool `json:"autoRenew"` + KeyType string `json:"keyType"` + Apply bool `json:"apply"` + PushDir bool `json:"pushDir"` + Dir string `json:"dir"` + ID uint `json:"id"` + Description string `json:"description"` + DisableCNAME bool `json:"disableCNAME"` + SkipDNS bool `json:"skipDNS"` + Nameserver1 string `json:"nameserver1"` + Nameserver2 string `json:"nameserver2"` + ExecShell bool `json:"execShell"` + Shell string `json:"shell"` +} + +type WebsiteDNSReq struct { + Domains []string `json:"domains" validate:"required"` + AcmeAccountID uint `json:"acmeAccountId" validate:"required"` +} + +type WebsiteSSLRenew struct { + SSLID uint `json:"SSLId" validate:"required"` +} + +type WebsiteSSLApply struct { + ID uint `json:"ID" validate:"required"` + SkipDNSCheck bool `json:"skipDNSCheck"` + Nameservers []string `json:"nameservers"` +} + +type WebsiteAcmeAccountCreate struct { + Email string `json:"email" validate:"required"` + Type string `json:"type" validate:"required,oneof=letsencrypt zerossl buypass google"` + KeyType string `json:"keyType" validate:"required,oneof=P256 P384 2048 3072 4096 8192"` + EabKid string `json:"eabKid"` + EabHmacKey string `json:"eabHmacKey"` +} + +type WebsiteDnsAccountCreate struct { + Name string `json:"name" validate:"required"` + Type string `json:"type" validate:"required"` + Authorization map[string]string `json:"authorization" validate:"required"` +} + +type WebsiteDnsAccountUpdate struct { + ID uint `json:"id" validate:"required"` + Name string `json:"name" validate:"required"` + Type string `json:"type" validate:"required"` + Authorization map[string]string `json:"authorization" validate:"required"` +} + +type WebsiteResourceReq struct { + ID uint `json:"id" validate:"required"` +} + +type WebsiteBatchDelReq struct { + IDs []uint `json:"ids" validate:"required"` +} + +type WebsiteSSLUpdate struct { + ID uint `json:"id" validate:"required"` + AutoRenew bool `json:"autoRenew"` + Description string `json:"description"` + PrimaryDomain string `json:"primaryDomain" validate:"required"` + OtherDomains string `json:"otherDomains"` + Provider string `json:"provider" validate:"required"` + AcmeAccountID uint `json:"acmeAccountId"` + DnsAccountID uint `json:"dnsAccountId"` + KeyType string `json:"keyType"` + Apply bool `json:"apply"` + PushDir bool `json:"pushDir"` + Dir string `json:"dir"` + DisableCNAME bool `json:"disableCNAME"` + SkipDNS bool `json:"skipDNS"` + Nameserver1 string `json:"nameserver1"` + Nameserver2 string `json:"nameserver2"` + ExecShell bool `json:"execShell"` + Shell string `json:"shell"` +} + +type WebsiteSSLUpload struct { + PrivateKey string `json:"privateKey"` + Certificate string `json:"certificate"` + PrivateKeyPath string `json:"privateKeyPath"` + CertificatePath string `json:"certificatePath"` + Type string `json:"type" validate:"required,oneof=paste local"` + SSLID uint `json:"sslID"` + Description string `json:"description"` +} + +type WebsiteCASearch struct { + dto.PageInfo +} + +type WebsiteCACreate struct { + CommonName string `json:"commonName" validate:"required"` + Country string `json:"country" validate:"required"` + Organization string `json:"organization" validate:"required"` + OrganizationUint string `json:"organizationUint"` + Name string `json:"name" validate:"required"` + KeyType string `json:"keyType" validate:"required,oneof=P256 P384 2048 3072 4096 8192"` + Province string `json:"province" ` + City string `json:"city"` +} + +type WebsiteCAObtain struct { + ID uint `json:"id" validate:"required"` + Domains string `json:"domains" validate:"required"` + KeyType string `json:"keyType" validate:"required,oneof=P256 P384 2048 3072 4096 8192"` + Time int `json:"time" validate:"required"` + Unit string `json:"unit" validate:"required"` + PushDir bool `json:"pushDir"` + Dir string `json:"dir"` + AutoRenew bool `json:"autoRenew"` + Renew bool `json:"renew"` + SSLID uint `json:"sslID"` + Description string `json:"description"` + ExecShell bool `json:"execShell"` + Shell string `json:"shell"` +} + +type WebsiteCARenew struct { + SSLID uint `json:"SSLID" validate:"required"` +} diff --git a/agent/app/dto/response/app.go b/agent/app/dto/response/app.go new file mode 100644 index 000000000..0ca1b0bed --- /dev/null +++ b/agent/app/dto/response/app.go @@ -0,0 +1,154 @@ +package response + +import ( + "time" + + "github.com/1Panel-dev/1Panel/agent/app/dto" + "github.com/1Panel-dev/1Panel/agent/app/dto/request" + + "github.com/1Panel-dev/1Panel/agent/app/model" +) + +type AppRes struct { + Items []*AppDto `json:"items"` + Total int64 `json:"total"` +} + +type AppUpdateRes struct { + CanUpdate bool `json:"canUpdate"` + IsSyncing bool `json:"isSyncing"` + AppStoreLastModified int `json:"appStoreLastModified"` + AppList *dto.AppList `json:"appList"` +} + +type AppDTO struct { + model.App + Installed bool `json:"installed"` + Versions []string `json:"versions"` + Tags []model.Tag `json:"tags"` +} + +type AppDto struct { + Name string `json:"name"` + Key string `json:"key"` + ID uint `json:"id"` + ShortDescZh string `json:"shortDescZh"` + ShortDescEn string `json:"shortDescEn"` + Icon string `json:"icon"` + Type string `json:"type"` + Status string `json:"status"` + Resource string `json:"resource"` + Installed bool `json:"installed"` + Versions []string `json:"versions"` + Limit int `json:"limit"` + Tags []model.Tag `json:"tags"` +} + +type TagDTO struct { + model.Tag +} + +type AppInstalledCheck struct { + IsExist bool `json:"isExist"` + Name string `json:"name"` + App string `json:"app"` + Version string `json:"version"` + Status string `json:"status"` + CreatedAt time.Time `json:"createdAt"` + LastBackupAt string `json:"lastBackupAt"` + AppInstallID uint `json:"appInstallId"` + ContainerName string `json:"containerName"` + InstallPath string `json:"installPath"` + HttpPort int `json:"httpPort"` + HttpsPort int `json:"httpsPort"` +} + +type AppDetailDTO struct { + model.AppDetail + Enable bool `json:"enable"` + Params interface{} `json:"params"` + Image string `json:"image"` + HostMode bool `json:"hostMode"` +} + +type IgnoredApp struct { + Icon string `json:"icon"` + Name string `json:"name"` + Version string `json:"version"` + DetailID uint `json:"detailID"` +} + +type AppInstalledDTO struct { + model.AppInstall + Total int `json:"total"` + Ready int `json:"ready"` + AppName string `json:"appName"` + Icon string `json:"icon"` + CanUpdate bool `json:"canUpdate"` + Path string `json:"path"` +} + +type AppDetail struct { + Website string `json:"website"` + Document string `json:"document"` + Github string `json:"github"` +} + +type AppInstallDTO struct { + ID uint `json:"id"` + Name string `json:"name"` + AppID uint `json:"appID"` + AppDetailID uint `json:"appDetailID"` + Version string `json:"version"` + Status string `json:"status"` + Message string `json:"message"` + HttpPort int `json:"httpPort"` + HttpsPort int `json:"httpsPort"` + Path string `json:"path"` + CanUpdate bool `json:"canUpdate"` + Icon string `json:"icon"` + AppName string `json:"appName"` + Ready int `json:"ready"` + Total int `json:"total"` + AppKey string `json:"appKey"` + AppType string `json:"appType"` + AppStatus string `json:"appStatus"` + DockerCompose string `json:"dockerCompose"` + CreatedAt time.Time `json:"createdAt"` + App AppDetail `json:"app"` +} + +type DatabaseConn struct { + Status string `json:"status"` + Username string `json:"username"` + Password string `json:"password"` + ContainerName string `json:"containerName"` + ServiceName string `json:"serviceName"` + Port int64 `json:"port"` +} + +type AppService struct { + Label string `json:"label"` + Value string `json:"value"` + Config interface{} `json:"config"` + From string `json:"from"` +} + +type AppParam struct { + Value interface{} `json:"value"` + Edit bool `json:"edit"` + Key string `json:"key"` + Rule string `json:"rule"` + LabelZh string `json:"labelZh"` + LabelEn string `json:"labelEn"` + Type string `json:"type"` + Values interface{} `json:"values"` + ShowValue string `json:"showValue"` + Required bool `json:"required"` + Multiple bool `json:"multiple"` +} + +type AppConfig struct { + Params []AppParam `json:"params"` + request.AppContainerConfig +} diff --git a/agent/app/dto/response/favorite.go b/agent/app/dto/response/favorite.go new file mode 100644 index 000000000..9998978d5 --- /dev/null +++ b/agent/app/dto/response/favorite.go @@ -0,0 +1,7 @@ +package response + +import "github.com/1Panel-dev/1Panel/agent/app/model" + +type FavoriteDTO struct { + model.Favorite +} diff --git a/agent/app/dto/response/file.go b/agent/app/dto/response/file.go new file mode 100644 index 000000000..ae756cedd --- /dev/null +++ b/agent/app/dto/response/file.go @@ -0,0 +1,47 @@ +package response + +import ( + "github.com/1Panel-dev/1Panel/agent/utils/files" +) + +type FileInfo struct { + files.FileInfo +} + +type UploadInfo struct { + Name string `json:"name"` + Size int `json:"size"` + CreatedAt string `json:"createdAt"` +} + +type FileTree struct { + ID string `json:"id"` + Name string `json:"name"` + Path string `json:"path"` + IsDir bool `json:"isDir"` + Extension string `json:"extension"` + Children []FileTree `json:"children"` +} + +type DirSizeRes struct { + Size float64 `json:"size" validate:"required"` +} + +type FileProcessKeys struct { + Keys []string `json:"keys"` +} + +type FileWgetRes struct { + Key string `json:"key"` +} + +type FileLineContent struct { + Content string `json:"content"` + End bool `json:"end"` + Path string `json:"path"` + Total int `json:"total"` +} + +type FileExist struct { + Exist bool `json:"exist"` +} diff --git a/agent/app/dto/response/host_tool.go b/agent/app/dto/response/host_tool.go new file mode 100644 index 000000000..e97a675d8 --- /dev/null +++ b/agent/app/dto/response/host_tool.go @@ -0,0 +1,41 @@ +package response + +type HostToolRes struct { + Type string `json:"type"` + Config interface{} `json:"config"` +} + +type Supervisor struct { + ConfigPath string `json:"configPath"` + IncludeDir string `json:"includeDir"` + LogPath string `json:"logPath"` + IsExist bool `json:"isExist"` + Init bool `json:"init"` + Msg string `json:"msg"` + Version string `json:"version"` + Status string `json:"status"` + CtlExist bool `json:"ctlExist"` + ServiceName string `json:"serviceName"` +} + +type HostToolConfig struct { + Content string `json:"content"` +} + +type SupervisorProcessConfig struct { + Name string `json:"name"` + Command string `json:"command"` + User string `json:"user"` + Dir string `json:"dir"` + Numprocs string `json:"numprocs"` + Msg string `json:"msg"` + Status []ProcessStatus `json:"status"` +} + +type ProcessStatus struct { + Name string `json:"name"` + Status string `json:"status"` + PID string `json:"PID"` + Uptime string `json:"uptime"` + Msg string `json:"msg"` +} diff --git a/agent/app/dto/response/nginx.go b/agent/app/dto/response/nginx.go new file mode 100644 index 000000000..e7d787787 --- /dev/null +++ b/agent/app/dto/response/nginx.go @@ -0,0 +1,55 @@ +package response + +import "github.com/1Panel-dev/1Panel/agent/app/dto" + +type NginxStatus struct { + Active string `json:"active"` + Accepts string `json:"accepts"` + Handled string `json:"handled"` + Requests string `json:"requests"` + Reading string `json:"reading"` + Writing string `json:"writing"` + Waiting string `json:"waiting"` +} + +type NginxParam struct { + Name string `json:"name"` + Params []string `json:"params"` +} + +type NginxAuthRes struct { + Enable bool `json:"enable"` + Items []dto.NginxAuth `json:"items"` +} + +type NginxAntiLeechRes struct { + Enable bool `json:"enable"` + Extends string `json:"extends"` + Return string `json:"return"` + ServerNames []string `json:"serverNames"` + Cache bool `json:"cache"` + CacheTime int `json:"cacheTime"` + CacheUint string `json:"cacheUint"` + NoneRef bool `json:"noneRef"` + LogEnable bool `json:"logEnable"` + Blocked bool `json:"blocked"` +} + +type NginxRedirectConfig struct { + WebsiteID uint `json:"websiteID"` + Name string `json:"name"` + Domains []string `json:"domains"` + KeepPath bool `json:"keepPath"` + Enable bool `json:"enable"` + Type string `json:"type"` + Redirect string `json:"redirect"` + Path string `json:"path"` + Target string `json:"target"` + FilePath string `json:"filePath"` + Content string `json:"content"` + RedirectRoot bool `json:"redirectRoot"` +} + +type NginxFile struct { + Content string `json:"content"` +} diff --git a/agent/app/dto/response/php_extensions.go b/agent/app/dto/response/php_extensions.go new file mode 100644 index 000000000..f3d11b3fd --- /dev/null +++ b/agent/app/dto/response/php_extensions.go @@ -0,0 +1,7 @@ +package response + +import "github.com/1Panel-dev/1Panel/agent/app/model" + +type PHPExtensionsDTO struct { + model.PHPExtensions +} diff --git a/agent/app/dto/response/recycle_bin.go b/agent/app/dto/response/recycle_bin.go new file mode 100644 index 000000000..b5a040358 --- /dev/null +++ b/agent/app/dto/response/recycle_bin.go @@ -0,0 +1,14 @@ +package response + +import "time" + +type RecycleBinDTO struct { + Name string `json:"name"` + Size int `json:"size"` + Type string `json:"type"` + DeleteTime time.Time `json:"deleteTime"` + RName string `json:"rName"` + SourcePath string `json:"sourcePath"` + IsDir bool `json:"isDir"` + From string `json:"from"` +} diff --git a/agent/app/dto/response/runtime.go b/agent/app/dto/response/runtime.go new file mode 100644 index 000000000..25d408f6f --- /dev/null +++ b/agent/app/dto/response/runtime.go @@ -0,0 +1,59 @@ +package response + +import ( + "time" + + "github.com/1Panel-dev/1Panel/agent/app/dto/request" + "github.com/1Panel-dev/1Panel/agent/app/model" +) + +type RuntimeDTO struct { + ID uint `json:"id"` + Name string `json:"name"` + Resource string `json:"resource"` + AppDetailID uint `json:"appDetailID"` + AppID uint `json:"appID"` + Source string `json:"source"` + Status string `json:"status"` + Type string `json:"type"` + Image string `json:"image"` + Params map[string]interface{} `json:"params"` + Message string `json:"message"` + Version string `json:"version"` + CreatedAt time.Time `json:"createdAt"` + CodeDir string `json:"codeDir"` + AppParams []AppParam `json:"appParams"` + Port int `json:"port"` + Path string `json:"path"` + ExposedPorts []request.ExposedPort `json:"exposedPorts"` +} + +type PackageScripts struct { + Name string `json:"name"` + Script string `json:"script"` +} + +func NewRuntimeDTO(runtime model.Runtime) RuntimeDTO { + return RuntimeDTO{ + ID: runtime.ID, + Name: runtime.Name, + Resource: runtime.Resource, + AppDetailID: runtime.AppDetailID, + Status: runtime.Status, + Type: runtime.Type, + Image: runtime.Image, + Message: runtime.Message, + CreatedAt: runtime.CreatedAt, + CodeDir: runtime.CodeDir, + Version: runtime.Version, + Port: runtime.Port, + Path: runtime.GetPath(), + } +} + +type NodeModule struct { + Name string `json:"name"` + Version string `json:"version"` + License string `json:"license"` + Description string `json:"description"` +} diff --git a/agent/app/dto/response/website.go b/agent/app/dto/response/website.go new file mode 100644 index 000000000..b99c4fa2a --- /dev/null +++ b/agent/app/dto/response/website.go @@ -0,0 +1,89 @@ +package response + +import ( + "time" + + "github.com/1Panel-dev/1Panel/agent/app/model" +) + +type WebsiteDTO struct { + model.Website + ErrorLogPath string `json:"errorLogPath"` + AccessLogPath string `json:"accessLogPath"` + SitePath string `json:"sitePath"` + AppName string `json:"appName"` + RuntimeName string `json:"runtimeName"` + SiteDir string `gorm:"type:varchar;" json:"siteDir"` +} + +type WebsiteRes struct { + ID uint `json:"id"` + CreatedAt time.Time `json:"createdAt"` + Protocol string `json:"protocol"` + PrimaryDomain string `json:"primaryDomain"` + Type string `json:"type"` + Alias string `json:"alias"` + Remark string `json:"remark"` + Status string `json:"status"` + ExpireDate time.Time `json:"expireDate"` + SitePath string `json:"sitePath"` + AppName string `json:"appName"` + RuntimeName string `json:"runtimeName"` + SSLExpireDate time.Time `json:"sslExpireDate"` + SSLStatus string `json:"sslStatus"` +} + +type WebsiteOption struct { + ID uint `json:"id"` + PrimaryDomain string `json:"primaryDomain"` + Alias string `json:"alias"` +} + +type WebsitePreInstallCheck struct { + Name string `json:"name"` + Status string `json:"status"` + Version string `json:"version"` + AppName string `json:"appName"` +} + +type WebsiteNginxConfig struct { + Enable bool `json:"enable"` + Params []NginxParam `json:"params"` +} + +type WebsiteHTTPS struct { + Enable bool `json:"enable"` + HttpConfig string `json:"httpConfig"` + SSL model.WebsiteSSL `json:"SSL"` + SSLProtocol []string `json:"SSLProtocol"` + Algorithm string `json:"algorithm"` + Hsts bool `json:"hsts"` +} + +type WebsiteLog struct { + Enable bool `json:"enable"` + Content string `json:"content"` + End bool `json:"end"` + Path string `json:"path"` +} + +type PHPConfig struct { + Params map[string]string `json:"params"` + DisableFunctions []string `json:"disableFunctions"` + UploadMaxSize string `json:"uploadMaxSize"` +} + +type NginxRewriteRes struct { + Content string `json:"content"` +} + +type WebsiteDirConfig struct { + Dirs []string `json:"dirs"` + User string `json:"user"` + UserGroup string `json:"userGroup"` + Msg string `json:"msg"` +} + +type WebsiteHtmlRes struct { + Content string `json:"content"` +} diff --git a/agent/app/dto/response/website_ssl.go b/agent/app/dto/response/website_ssl.go new file mode 100644 index 000000000..8afb4882e --- /dev/null +++ b/agent/app/dto/response/website_ssl.go @@ -0,0 +1,34 @@ +package response + +import "github.com/1Panel-dev/1Panel/agent/app/model" + +type WebsiteSSLDTO struct { + model.WebsiteSSL + LogPath string `json:"logPath"` +} + +type WebsiteDNSRes struct { + Key string `json:"resolve"` + Value string `json:"value"` + Domain string `json:"domain"` + Err string `json:"err"` +} + +type WebsiteAcmeAccountDTO struct { + model.WebsiteAcmeAccount +} + +type WebsiteDnsAccountDTO struct { + model.WebsiteDnsAccount + Authorization map[string]string `json:"authorization"` +} + +type WebsiteCADTO struct { + model.WebsiteCA + CommonName string `json:"commonName" ` + Country string `json:"country"` + Organization string `json:"organization"` + OrganizationUint string `json:"organizationUint"` + Province string `json:"province" ` + City string `json:"city"` +} diff --git a/agent/app/dto/setting.go b/agent/app/dto/setting.go new file mode 100644 index 000000000..66c510c4e --- /dev/null +++ b/agent/app/dto/setting.go @@ -0,0 +1,124 @@ +package dto + +import "time" + +type SettingInfo struct { + SystemIP string `json:"systemIP"` + DockerSockPath string `json:"dockerSockPath"` + SystemVersion string `json:"systemVersion"` + + LocalTime string `json:"localTime"` + TimeZone string `json:"timeZone"` + NtpSite string `json:"ntpSite"` + + DefaultNetwork string `json:"defaultNetwork"` + LastCleanTime string `json:"lastCleanTime"` + LastCleanSize string `json:"lastCleanSize"` + LastCleanData string `json:"lastCleanData"` + + MonitorStatus string `json:"monitorStatus"` + MonitorInterval string `json:"monitorInterval"` + MonitorStoreDays string `json:"monitorStoreDays"` + + AppStoreVersion string `json:"appStoreVersion"` + AppStoreLastModified string `json:"appStoreLastModified"` + AppStoreSyncStatus string `json:"appStoreSyncStatus"` + + FileRecycleBin string `json:"fileRecycleBin"` + + SnapshotIgnore string `json:"snapshotIgnore"` +} + +type SettingUpdate struct { + Key string `json:"key" validate:"required"` + Value string `json:"value"` +} + +type SnapshotStatus struct { + Panel string `json:"panel"` + PanelInfo string `json:"panelInfo"` + DaemonJson string `json:"daemonJson"` + AppData string `json:"appData"` + PanelData string `json:"panelData"` + BackupData string `json:"backupData"` + + Compress string `json:"compress"` + Size string `json:"size"` + Upload string `json:"upload"` +} + +type SnapshotCreate struct { + ID uint `json:"id"` + From string `json:"from" validate:"required"` + DefaultDownload string `json:"defaultDownload" validate:"required"` + Description string `json:"description" validate:"max=256"` + Secret string `json:"secret"` +} +type SnapshotRecover struct { + IsNew bool `json:"isNew"` + ReDownload bool `json:"reDownload"` + ID uint `json:"id" validate:"required"` + Secret string `json:"secret"` +} +type SnapshotBatchDelete struct { + DeleteWithFile bool `json:"deleteWithFile"` + Ids []uint `json:"ids" validate:"required"` +} + +type SnapshotImport struct { + From string `json:"from"` + Names []string `json:"names"` + Description string `json:"description" validate:"max=256"` +} + +type SnapshotInfo struct { + ID uint `json:"id"` + Name string `json:"name"` + Description string `json:"description" validate:"max=256"` + From string `json:"from"` + DefaultDownload string `json:"defaultDownload"` + Status string `json:"status"` + Message string `json:"message"` + CreatedAt time.Time `json:"createdAt"` + Version string `json:"version"` + Size int64 `json:"size"` + + InterruptStep string `json:"interruptStep"` + RecoverStatus string `json:"recoverStatus"` + RecoverMessage string `json:"recoverMessage"` + LastRecoveredAt string `json:"lastRecoveredAt"` + RollbackStatus string `json:"rollbackStatus"` + RollbackMessage string `json:"rollbackMessage"` + LastRollbackedAt string `json:"lastRollbackedAt"` +} + +type SyncTime struct { + NtpSite string `json:"ntpSite" validate:"required"` +} + +type CleanData struct { + SystemClean []CleanTree `json:"systemClean"` + UploadClean []CleanTree `json:"uploadClean"` + DownloadClean []CleanTree `json:"downloadClean"` + SystemLogClean []CleanTree `json:"systemLogClean"` + ContainerClean []CleanTree `json:"containerClean"` +} + +type CleanTree struct { + ID string `json:"id"` + Label string `json:"label"` + Children []CleanTree `json:"children"` + + Type string `json:"type"` + Name string `json:"name"` + + Size uint64 `json:"size"` + IsCheck bool `json:"isCheck"` + IsRecommend bool `json:"isRecommend"` +} + +type Clean struct { + TreeType string `json:"treeType"` + Name string `json:"name"` + Size uint64 `json:"size"` +} diff --git a/agent/app/dto/ssh.go b/agent/app/dto/ssh.go new file mode 100644 index 000000000..c07a3f4e9 --- /dev/null +++ b/agent/app/dto/ssh.go @@ -0,0 +1,57 @@ +package dto + +import "time" + +type SSHUpdate struct { + Key string `json:"key" validate:"required"` + OldValue string `json:"oldValue"` + NewValue string `json:"newValue"` +} + +type SSHInfo struct { + AutoStart bool `json:"autoStart"` + Status string `json:"status"` + Message string `json:"message"` + Port string `json:"port"` + ListenAddress string `json:"listenAddress"` + PasswordAuthentication string `json:"passwordAuthentication"` + PubkeyAuthentication string `json:"pubkeyAuthentication"` + PermitRootLogin string `json:"permitRootLogin"` + UseDNS string `json:"useDNS"` +} + +type GenerateSSH struct { + EncryptionMode string `json:"encryptionMode" validate:"required,oneof=rsa ed25519 ecdsa dsa"` + Password string `json:"password"` +} + +type GenerateLoad struct { + EncryptionMode string `json:"encryptionMode" validate:"required,oneof=rsa ed25519 ecdsa dsa"` +} + +type SSHConf struct { + File string `json:"file"` +} +type SearchSSHLog struct { + PageInfo + Info string `json:"info"` + Status string `json:"Status" validate:"required,oneof=Success Failed All"` +} +type SSHLog struct { + Logs []SSHHistory `json:"logs"` + TotalCount int `json:"totalCount"` + SuccessfulCount int `json:"successfulCount"` + FailedCount int `json:"failedCount"` +} + +type SSHHistory struct { + Date time.Time `json:"date"` + DateStr string `json:"dateStr"` + Area string `json:"area"` + User string `json:"user"` + AuthMode string `json:"authMode"` + Address string `json:"address"` + Port string `json:"port"` + Status string `json:"status"` + Message string `json:"message"` +} diff --git a/agent/app/model/app.go b/agent/app/model/app.go new file mode 100644 index 000000000..a5d1bc391 --- /dev/null +++ b/agent/app/model/app.go @@ -0,0 +1,44 @@ +package model + +import ( + "path/filepath" + "strings" + + "github.com/1Panel-dev/1Panel/agent/constant" +) + +type App struct { + BaseModel + Name string `json:"name" gorm:"type:varchar(64);not null"` + Key string `json:"key" gorm:"type:varchar(64);not null;"` + ShortDescZh string `json:"shortDescZh" yaml:"shortDescZh" gorm:"type:longtext;"` + ShortDescEn string `json:"shortDescEn" yaml:"shortDescEn" gorm:"type:longtext;"` + Icon string `json:"icon" gorm:"type:longtext;"` + Type string `json:"type" gorm:"type:varchar(64);not null"` + Status string `json:"status" gorm:"type:varchar(64);not null"` + Required string `json:"required" gorm:"type:varchar(64);"` + CrossVersionUpdate bool `json:"crossVersionUpdate" yaml:"crossVersionUpdate"` + Limit int `json:"limit" gorm:"type:Integer;not null"` + Website string `json:"website" gorm:"type:varchar(64);not null"` + Github string `json:"github" gorm:"type:varchar(64);not null"` + Document string `json:"document" gorm:"type:varchar(64);not null"` + Recommend int `json:"recommend" gorm:"type:Integer;not null"` + Resource string `json:"resource" gorm:"type:varchar;not null;default:remote"` + ReadMe string `json:"readMe" gorm:"type:varchar;"` + LastModified int `json:"lastModified" gorm:"type:Integer;"` + + Details []AppDetail `json:"-" gorm:"-:migration"` + TagsKey []string `json:"tags" yaml:"tags" gorm:"-"` + AppTags []AppTag `json:"-" gorm:"-:migration"` +} + +func (i *App) IsLocalApp() bool { + return i.Resource == constant.ResourceLocal +} +func (i *App) GetAppResourcePath() string { + if i.IsLocalApp() { + //这里要去掉本地应用的local前缀 + return filepath.Join(constant.LocalAppResourceDir, strings.TrimPrefix(i.Key, "local")) + } + return filepath.Join(constant.RemoteAppResourceDir, i.Key) +} diff --git a/agent/app/model/app_detail.go b/agent/app/model/app_detail.go new file mode 100644 index 000000000..d6b958284 --- /dev/null +++ b/agent/app/model/app_detail.go @@ -0,0 +1,16 @@ +package model + +type AppDetail struct { + BaseModel + AppId uint `json:"appId" gorm:"type:integer;not null"` + Version string `json:"version" gorm:"type:varchar(64);not null"` + Params string `json:"-" gorm:"type:longtext;"` + DockerCompose string `json:"dockerCompose" gorm:"type:longtext;"` + Status string `json:"status" gorm:"type:varchar(64);not null"` + LastVersion string `json:"lastVersion" gorm:"type:varchar(64);"` + LastModified int `json:"lastModified" gorm:"type:integer;"` + DownloadUrl string `json:"downloadUrl" gorm:"type:varchar;"` + DownloadCallBackUrl string `json:"downloadCallBackUrl" gorm:"type:longtext;"` + Update bool `json:"update"` + IgnoreUpgrade bool `json:"ignoreUpgrade"` +} diff --git a/agent/app/model/app_install.go b/agent/app/model/app_install.go new file mode 100644 index 000000000..3d3829952 --- /dev/null +++ b/agent/app/model/app_install.go @@ -0,0 +1,47 @@ +package model + +import ( + "path" + "strings" + + "github.com/1Panel-dev/1Panel/agent/constant" +) + +type AppInstall struct { + BaseModel + Name string `json:"name" gorm:"type:varchar(64);not null;UNIQUE"` + AppId uint `json:"appId" gorm:"type:integer;not null"` + AppDetailId uint `json:"appDetailId" gorm:"type:integer;not null"` + Version string `json:"version" gorm:"type:varchar(64);not null"` + Param string `json:"param" gorm:"type:longtext;"` + Env string `json:"env" gorm:"type:longtext;"` + DockerCompose string `json:"dockerCompose" gorm:"type:longtext;"` + Status string `json:"status" gorm:"type:varchar(256);not null"` + Description string `json:"description" gorm:"type:varchar(256);"` + Message string `json:"message" gorm:"type:longtext;"` + ContainerName string `json:"containerName" gorm:"type:varchar(256);not null"` + ServiceName string `json:"serviceName" gorm:"type:varchar(256);not null"` + HttpPort int `json:"httpPort" gorm:"type:integer;not null"` + HttpsPort int `json:"httpsPort" gorm:"type:integer;not null"` + App App `json:"app" gorm:"-:migration"` +} + +func (i *AppInstall) GetPath() string { + return path.Join(i.GetAppPath(), i.Name) +} + +func (i *AppInstall) GetComposePath() string { + return path.Join(i.GetAppPath(), i.Name, "docker-compose.yml") +} + +func (i *AppInstall) GetEnvPath() string { + return path.Join(i.GetAppPath(), i.Name, ".env") +} + +func (i *AppInstall) GetAppPath() string { + if i.App.Resource == constant.AppResourceLocal { + return path.Join(constant.LocalAppInstallDir, strings.TrimPrefix(i.App.Key, constant.AppResourceLocal)) + } else { + return path.Join(constant.AppInstallDir, i.App.Key) + } +} diff --git a/agent/app/model/app_install_resource.go b/agent/app/model/app_install_resource.go new file mode 100644 index 000000000..863090459 --- /dev/null +++ b/agent/app/model/app_install_resource.go @@ -0,0 +1,10 @@ +package model + +type AppInstallResource struct { + BaseModel + AppInstallId uint `json:"appInstallId" gorm:"type:integer;not null;"` + LinkId uint `json:"linkId" gorm:"type:integer;not null;"` + ResourceId uint `json:"resourceId" gorm:"type:integer;"` + Key string `json:"key" gorm:"type:varchar(64);not null"` + From string `json:"from" gorm:"type:varchar(64);not null;default:local"` +} diff --git a/agent/app/model/app_tag.go b/agent/app/model/app_tag.go new file mode 100644 index 000000000..1d55b956a --- /dev/null +++ b/agent/app/model/app_tag.go @@ -0,0 +1,7 @@ +package model + +type AppTag struct { + BaseModel + AppId uint `json:"appId" gorm:"type:integer;not null"` + TagId uint `json:"tagId" gorm:"type:integer;not null"` +} diff --git a/agent/app/model/backup.go b/agent/app/model/backup.go new file mode 100644 index 000000000..50690dcaa --- /dev/null +++ b/agent/app/model/backup.go @@ -0,0 +1,24 @@ +package model + +type BackupAccount struct { + BaseModel + Type string `gorm:"type:varchar(64);unique;not null" json:"type"` + Bucket string `gorm:"type:varchar(256)" json:"bucket"` + AccessKey string `gorm:"type:varchar(256)" json:"accessKey"` + Credential string `gorm:"type:varchar(256)" json:"credential"` + BackupPath string `gorm:"type:varchar(256)" json:"backupPath"` + Vars string `gorm:"type:longText" json:"vars"` +} + +type BackupRecord struct { + BaseModel + From string `gorm:"type:varchar(64)" json:"from"` + CronjobID uint `gorm:"type:decimal" json:"cronjobID"` + Type string `gorm:"type:varchar(64);not null" json:"type"` + Name string `gorm:"type:varchar(64);not null" json:"name"` + DetailName string `gorm:"type:varchar(256)" json:"detailName"` + Source string `gorm:"type:varchar(256)" json:"source"` + BackupType string `gorm:"type:varchar(256)" json:"backupType"` + FileDir string `gorm:"type:varchar(256)" json:"fileDir"` + FileName string `gorm:"type:varchar(256)" json:"fileName"` +} diff --git a/agent/app/model/base.go b/agent/app/model/base.go new file mode 100644 index 000000000..69921d41d --- /dev/null +++ b/agent/app/model/base.go @@ -0,0 +1,9 @@ +package model + +import "time" + +type BaseModel struct { + ID uint `gorm:"primarykey;AUTO_INCREMENT" json:"id"` + CreatedAt time.Time `json:"createdAt"` + UpdatedAt time.Time `json:"updatedAt"` +} diff --git a/agent/app/model/clam.go b/agent/app/model/clam.go new file mode 100644 index 000000000..ff1a73e67 --- /dev/null +++ b/agent/app/model/clam.go @@ -0,0 +1,14 @@ +package model + +type Clam struct { + BaseModel + + Name string `gorm:"type:varchar(64);not null" json:"name"` + Status string `gorm:"type:varchar(64)" json:"status"` + Path string `gorm:"type:varchar(64);not null" json:"path"` + InfectedStrategy string `gorm:"type:varchar(64)" json:"infectedStrategy"` + InfectedDir string `gorm:"type:varchar(64)" json:"infectedDir"` + Spec string `gorm:"type:varchar(64)" json:"spec"` + EntryID int `gorm:"type:varchar(64)" json:"entryID"` + Description string `gorm:"type:varchar(64)" json:"description"` +} diff --git a/agent/app/model/command.go b/agent/app/model/command.go new file mode 100644 index 000000000..9f00558d4 --- /dev/null +++ b/agent/app/model/command.go @@ -0,0 +1,14 @@ +package model + +type Command struct { + BaseModel + Name string `gorm:"type:varchar(64);unique;not null" json:"name"` + GroupID uint `gorm:"type:decimal" json:"groupID"` + Command string `gorm:"type:varchar(256);not null" json:"command"` +} + +type RedisCommand struct { + BaseModel + Name string `gorm:"type:varchar(64);unique;not null" json:"name"` + Command string `gorm:"type:varchar(256);not null" json:"command"` +} diff --git a/agent/app/model/compose_template.go b/agent/app/model/compose_template.go new file mode 100644 index 000000000..5d5192eb3 --- /dev/null +++ b/agent/app/model/compose_template.go @@ -0,0 +1,15 @@ +package model + +type ComposeTemplate struct { + BaseModel + + Name string `gorm:"type:varchar(64);not null;unique" json:"name"` + Description string `gorm:"type:varchar(256)" json:"description"` + Content string `gorm:"type:longtext" json:"content"` +} + +type Compose struct { + BaseModel + + Name string `gorm:"type:varchar(256)" json:"name"` +} diff --git a/agent/app/model/cronjob.go b/agent/app/model/cronjob.go new file mode 100644 index 000000000..b9ff73546 --- /dev/null +++ b/agent/app/model/cronjob.go @@ -0,0 +1,50 @@ +package model + +import ( + "time" +) + +type Cronjob struct { + BaseModel + + Name string `gorm:"type:varchar(64);not null" json:"name"` + Type string `gorm:"type:varchar(64);not null" json:"type"` + Spec string `gorm:"type:varchar(64);not null" json:"spec"` + + Command string `gorm:"type:varchar(64)" json:"command"` + ContainerName string `gorm:"type:varchar(64)" json:"containerName"` + Script string `gorm:"longtext" json:"script"` + Website string `gorm:"type:varchar(64)" json:"website"` + AppID string `gorm:"type:varchar(64)" json:"appID"` + DBType string `gorm:"type:varchar(64)" json:"dbType"` + DBName string `gorm:"type:varchar(64)" json:"dbName"` + URL string `gorm:"type:varchar(256)" json:"url"` + SourceDir string `gorm:"type:varchar(256)" json:"sourceDir"` + ExclusionRules string `gorm:"longtext" json:"exclusionRules"` + + // 已废弃 + KeepLocal bool `gorm:"type:varchar(64)" json:"keepLocal"` + TargetDirID uint64 `gorm:"type:decimal" json:"targetDirID"` + + BackupAccounts string `gorm:"type:varchar(64)" json:"backupAccounts"` + DefaultDownload string `gorm:"type:varchar(64)" json:"defaultDownload"` + RetainCopies uint64 `gorm:"type:decimal" json:"retainCopies"` + + Status string `gorm:"type:varchar(64)" json:"status"` + EntryIDs string `gorm:"type:varchar(64)" json:"entryIDs"` + Records []JobRecords `json:"records"` + Secret string `gorm:"type:varchar(64)" json:"secret"` +} + +type JobRecords struct { + BaseModel + + CronjobID uint `gorm:"type:decimal" json:"cronjobID"` + StartTime time.Time `gorm:"type:datetime" json:"startTime"` + Interval float64 `gorm:"type:float" json:"interval"` + Records string `gorm:"longtext" json:"records"` + FromLocal bool `gorm:"type:varchar(64)" json:"source"` + File string `gorm:"type:varchar(256)" json:"file"` + Status string `gorm:"type:varchar(64)" json:"status"` + Message string `gorm:"longtext" json:"message"` +} diff --git a/agent/app/model/database.go b/agent/app/model/database.go new file mode 100644 index 000000000..f4d0ff0fc --- /dev/null +++ b/agent/app/model/database.go @@ -0,0 +1,22 @@ +package model + +type Database struct { + BaseModel + AppInstallID uint `json:"appInstallID" gorm:"type:decimal"` + Name string `json:"name" gorm:"type:varchar(64);not null;unique"` + Type string `json:"type" gorm:"type:varchar(64);not null"` + Version string `json:"version" gorm:"type:varchar(64);not null"` + From string `json:"from" gorm:"type:varchar(64);not null"` + Address string `json:"address" gorm:"type:varchar(64);not null"` + Port uint `json:"port" gorm:"type:decimal;not null"` + Username string `json:"username" gorm:"type:varchar(64)"` + Password string `json:"password" gorm:"type:varchar(64)"` + + SSL bool `json:"ssl"` + RootCert string `json:"rootCert" gorm:"type:longText"` + ClientKey string `json:"clientKey" gorm:"type:longText"` + ClientCert string `json:"clientCert" gorm:"type:longText"` + SkipVerify bool `json:"skipVerify"` + + Description string `json:"description" gorm:"type:varchar(256);"` +} diff --git a/agent/app/model/database_mysql.go b/agent/app/model/database_mysql.go new file mode 100644 index 000000000..0a8734848 --- /dev/null +++ b/agent/app/model/database_mysql.go @@ -0,0 +1,14 @@ +package model + +type DatabaseMysql struct { + BaseModel + Name string `json:"name" gorm:"type:varchar(256);not null"` + From string `json:"from" gorm:"type:varchar(256);not null;default:local"` + MysqlName string `json:"mysqlName" gorm:"type:varchar(64);not null"` + 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"` + 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);"` +} diff --git a/agent/app/model/database_postgresql.go b/agent/app/model/database_postgresql.go new file mode 100644 index 000000000..7ea4ee59e --- /dev/null +++ b/agent/app/model/database_postgresql.go @@ -0,0 +1,14 @@ +package model + +type DatabasePostgresql struct { + BaseModel + Name string `json:"name" gorm:"type:varchar(256);not null"` + From string `json:"from" gorm:"type:varchar(256);not null;default:local"` + PostgresqlName string `json:"postgresqlName" gorm:"type:varchar(64);not null"` + 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)"` + IsDelete bool `json:"isDelete" gorm:"type:varchar(64)"` + Description string `json:"description" gorm:"type:varchar(256);"` +} diff --git a/agent/app/model/favorite.go b/agent/app/model/favorite.go new file mode 100644 index 000000000..6e5421dcf --- /dev/null +++ b/agent/app/model/favorite.go @@ -0,0 +1,10 @@ +package model + +type Favorite struct { + BaseModel + Name string `gorm:"type:varchar(256);not null;" json:"name" ` + Path string `gorm:"type:varchar(256);not null;unique" json:"path"` + Type string `gorm:"type:varchar(64);" json:"type"` + IsDir bool `json:"isDir"` + IsTxt bool `json:"isTxt"` +} diff --git a/agent/app/model/firewall.go b/agent/app/model/firewall.go new file mode 100644 index 000000000..d4979a05d --- /dev/null +++ b/agent/app/model/firewall.go @@ -0,0 +1,21 @@ +package model + +type Firewall struct { + BaseModel + + Type string `gorm:"type:varchar(64);not null" json:"type"` + Port string `gorm:"type:varchar(64);not null" json:"port"` + Protocol string `gorm:"type:varchar(64);not null" json:"protocol"` + Address string `gorm:"type:varchar(64);not null" json:"address"` + Strategy string `gorm:"type:varchar(64);not null" json:"strategy"` + Description string `gorm:"type:varchar(64);not null" json:"description"` +} + +type Forward struct { + BaseModel + + Protocol string `gorm:"type:varchar(64);not null" json:"protocol"` + Port string `gorm:"type:varchar(64);not null" json:"port"` + TargetIP string `gorm:"type:varchar(64);not null" json:"targetIP"` + TargetPort string `gorm:"type:varchar(64);not null" json:"targetPort"` +} diff --git a/agent/app/model/ftp.go b/agent/app/model/ftp.go new file mode 100644 index 000000000..291e765c6 --- /dev/null +++ b/agent/app/model/ftp.go @@ -0,0 +1,11 @@ +package model + +type Ftp struct { + BaseModel + + User string `gorm:"type:varchar(64);not null" json:"user"` + Password string `gorm:"type:varchar(64);not null" json:"password"` + Status string `gorm:"type:varchar(64);not null" json:"status"` + Path string `gorm:"type:varchar(64);not null" json:"path"` + Description string `gorm:"type:varchar(64);not null" json:"description"` +} diff --git a/agent/app/model/group.go b/agent/app/model/group.go new file mode 100644 index 000000000..1479e035a --- /dev/null +++ b/agent/app/model/group.go @@ -0,0 +1,8 @@ +package model + +type Group struct { + BaseModel + IsDefault bool `json:"isDefault"` + Name string `gorm:"type:varchar(64);not null" json:"name"` + Type string `gorm:"type:varchar(16);not null" json:"type"` +} diff --git a/agent/app/model/host.go b/agent/app/model/host.go new file mode 100644 index 000000000..a4fe6aedd --- /dev/null +++ b/agent/app/model/host.go @@ -0,0 +1,18 @@ +package model + +type Host struct { + BaseModel + + GroupID uint `gorm:"type:decimal;not null" json:"group_id"` + Name string `gorm:"type:varchar(64);not null" json:"name"` + Addr string `gorm:"type:varchar(16);not null" json:"addr"` + Port int `gorm:"type:decimal;not null" json:"port"` + User string `gorm:"type:varchar(64);not null" json:"user"` + AuthMode string `gorm:"type:varchar(16);not null" json:"authMode"` + Password string `gorm:"type:varchar(64)" json:"password"` + PrivateKey string `gorm:"type:varchar(256)" json:"privateKey"` + PassPhrase string `gorm:"type:varchar(256)" json:"passPhrase"` + RememberPassword bool `json:"rememberPassword"` + + Description string `gorm:"type:varchar(256)" json:"description"` +} diff --git a/agent/app/model/image_repo.go b/agent/app/model/image_repo.go new file mode 100644 index 000000000..8d16dbff1 --- /dev/null +++ b/agent/app/model/image_repo.go @@ -0,0 +1,15 @@ +package model + +type ImageRepo struct { + BaseModel + + Name string `gorm:"type:varchar(64);not null" json:"name"` + DownloadUrl string `gorm:"type:varchar(256)" json:"downloadUrl"` + Protocol string `gorm:"type:varchar(64)" json:"protocol"` + Username string `gorm:"type:varchar(256)" json:"username"` + Password string `gorm:"type:varchar(256)" json:"password"` + Auth bool `gorm:"type:varchar(256)" json:"auth"` + + Status string `gorm:"type:varchar(64)" json:"status"` + Message string `gorm:"type:varchar(256)" json:"message"` +} diff --git a/agent/app/model/monitor.go b/agent/app/model/monitor.go new file mode 100644 index 000000000..1207c98af --- /dev/null +++ b/agent/app/model/monitor.go @@ -0,0 +1,29 @@ +package model + +type MonitorBase struct { + BaseModel + Cpu float64 `gorm:"type:float" json:"cpu"` + + LoadUsage float64 `gorm:"type:float" json:"loadUsage"` + CpuLoad1 float64 `gorm:"type:float" json:"cpuLoad1"` + CpuLoad5 float64 `gorm:"type:float" json:"cpuLoad5"` + CpuLoad15 float64 `gorm:"type:float" json:"cpuLoad15"` + + Memory float64 `gorm:"type:float" json:"memory"` +} + +type MonitorIO struct { + BaseModel + Name string `json:"name"` + Read uint64 `json:"read"` + Write uint64 `json:"write"` + Count uint64 `json:"count"` + Time uint64 `json:"time"` +} + +type MonitorNetwork struct { + BaseModel + Name string `json:"name"` + Up float64 `gorm:"type:float" json:"up"` + Down float64 `gorm:"type:float" json:"down"` +} diff --git a/agent/app/model/php_extensions.go b/agent/app/model/php_extensions.go new file mode 100644 index 000000000..0055bd258 --- /dev/null +++ b/agent/app/model/php_extensions.go @@ -0,0 +1,7 @@ +package model + +type PHPExtensions struct { + BaseModel + Name string ` json:"name" gorm:"not null"` + Extensions string `json:"extensions" gorm:"not null"` +} diff --git a/agent/app/model/runtime.go b/agent/app/model/runtime.go new file mode 100644 index 000000000..4c9160276 --- /dev/null +++ b/agent/app/model/runtime.go @@ -0,0 +1,41 @@ +package model + +import ( + "path" + + "github.com/1Panel-dev/1Panel/agent/constant" +) + +type Runtime struct { + BaseModel + Name string `gorm:"type:varchar;not null" json:"name"` + AppDetailID uint `gorm:"type:integer" json:"appDetailId"` + Image string `gorm:"type:varchar" json:"image"` + WorkDir string `gorm:"type:varchar" json:"workDir"` + DockerCompose string `gorm:"type:varchar" json:"dockerCompose"` + Env string `gorm:"type:varchar" json:"env"` + Params string `gorm:"type:varchar" json:"params"` + Version string `gorm:"type:varchar;not null" json:"version"` + Type string `gorm:"type:varchar;not null" json:"type"` + Status string `gorm:"type:varchar;not null" json:"status"` + Resource string `gorm:"type:varchar;not null" json:"resource"` + Port int `gorm:"type:integer;" json:"port"` + Message string `gorm:"type:longtext;" json:"message"` + CodeDir string `gorm:"type:varchar;" json:"codeDir"` +} + +func (r *Runtime) GetComposePath() string { + return path.Join(r.GetPath(), "docker-compose.yml") +} + +func (r *Runtime) GetEnvPath() string { + return path.Join(r.GetPath(), ".env") +} + +func (r *Runtime) GetPath() string { + return path.Join(constant.RuntimeDir, r.Type, r.Name) +} + +func (r *Runtime) GetLogPath() string { + return path.Join(r.GetPath(), "build.log") +} diff --git a/agent/app/model/setting.go b/agent/app/model/setting.go new file mode 100644 index 000000000..e9791b79b --- /dev/null +++ b/agent/app/model/setting.go @@ -0,0 +1,8 @@ +package model + +type Setting struct { + BaseModel + Key string `json:"key" gorm:"type:varchar(256);not null;"` + Value string `json:"value" gorm:"type:varchar(256)"` + About string `json:"about" gorm:"type:longText"` +} diff --git a/agent/app/model/snapshot.go b/agent/app/model/snapshot.go new file mode 100644 index 000000000..09574ca65 --- /dev/null +++ b/agent/app/model/snapshot.go @@ -0,0 +1,35 @@ +package model + +type Snapshot struct { + BaseModel + Name string `json:"name" gorm:"type:varchar(64);not null;unique"` + Description string `json:"description" gorm:"type:varchar(256)"` + From string `json:"from"` + DefaultDownload string `json:"defaultDownload" gorm:"type:varchar(64)"` + Status string `json:"status" gorm:"type:varchar(64)"` + Message string `json:"message" gorm:"type:varchar(256)"` + Version string `json:"version" gorm:"type:varchar(256)"` + + InterruptStep string `json:"interruptStep" gorm:"type:varchar(64)"` + RecoverStatus string `json:"recoverStatus" gorm:"type:varchar(64)"` + RecoverMessage string `json:"recoverMessage" gorm:"type:varchar(256)"` + LastRecoveredAt string `json:"lastRecoveredAt" gorm:"type:varchar(64)"` + RollbackStatus string `json:"rollbackStatus" gorm:"type:varchar(64)"` + RollbackMessage string `json:"rollbackMessage" gorm:"type:varchar(256)"` + LastRollbackedAt string `json:"lastRollbackedAt" gorm:"type:varchar(64)"` +} + +type SnapshotStatus struct { + BaseModel + SnapID uint `gorm:"type:decimal" json:"snapID"` + Panel string `json:"panel" gorm:"type:varchar(64);default:Running"` + PanelInfo string `json:"panelInfo" gorm:"type:varchar(64);default:Running"` + DaemonJson string `json:"daemonJson" gorm:"type:varchar(64);default:Running"` + AppData string `json:"appData" gorm:"type:varchar(64);default:Running"` + PanelData string `json:"panelData" gorm:"type:varchar(64);default:Running"` + BackupData string `json:"backupData" gorm:"type:varchar(64);default:Running"` + + Compress string `json:"compress" gorm:"type:varchar(64);default:Waiting"` + Size string `json:"size" gorm:"type:varchar(64)"` + Upload string `json:"upload" gorm:"type:varchar(64);default:Waiting"` +} diff --git a/agent/app/model/tag.go b/agent/app/model/tag.go new file mode 100644 index 000000000..adc8d6ec4 --- /dev/null +++ b/agent/app/model/tag.go @@ -0,0 +1,8 @@ +package model + +type Tag struct { + BaseModel + Key string `json:"key" gorm:"type:varchar(64);not null"` + Name string `json:"name" gorm:"type:varchar(64);not null"` + Sort int `json:"sort" gorm:"type:int;not null;default:1"` +} diff --git a/agent/app/model/website.go b/agent/app/model/website.go new file mode 100644 index 000000000..34dc72ab4 --- /dev/null +++ b/agent/app/model/website.go @@ -0,0 +1,40 @@ +package model + +import "time" + +type Website struct { + BaseModel + Protocol string `gorm:"type:varchar;not null" json:"protocol"` + PrimaryDomain string `gorm:"type:varchar;not null" json:"primaryDomain"` + Type string `gorm:"type:varchar;not null" json:"type"` + Alias string `gorm:"type:varchar;not null" json:"alias"` + Remark string `gorm:"type:longtext;" json:"remark"` + Status string `gorm:"type:varchar;not null" json:"status"` + HttpConfig string `gorm:"type:varchar;not null" json:"httpConfig"` + ExpireDate time.Time `json:"expireDate"` + + Proxy string `gorm:"type:varchar;" json:"proxy"` + ProxyType string `gorm:"type:varchar;" json:"proxyType"` + SiteDir string `gorm:"type:varchar;" json:"siteDir"` + ErrorLog bool `json:"errorLog"` + AccessLog bool `json:"accessLog"` + DefaultServer bool `json:"defaultServer"` + IPV6 bool `json:"IPV6"` + Rewrite string `gorm:"type:varchar" json:"rewrite"` + + WebsiteGroupID uint `gorm:"type:integer" json:"webSiteGroupId"` + WebsiteSSLID uint `gorm:"type:integer" json:"webSiteSSLId"` + RuntimeID uint `gorm:"type:integer" json:"runtimeID"` + AppInstallID uint `gorm:"type:integer" json:"appInstallId"` + FtpID uint `gorm:"type:integer" json:"ftpId"` + + User string `gorm:"type:varchar;" json:"user"` + Group string `gorm:"type:varchar;" json:"group"` + + Domains []WebsiteDomain `json:"domains" gorm:"-:migration"` + WebsiteSSL WebsiteSSL `json:"webSiteSSL" gorm:"-:migration"` +} + +func (w Website) TableName() string { + return "websites" +} diff --git a/agent/app/model/website_acme_account.go b/agent/app/model/website_acme_account.go new file mode 100644 index 000000000..eeafc0187 --- /dev/null +++ b/agent/app/model/website_acme_account.go @@ -0,0 +1,16 @@ +package model + +type WebsiteAcmeAccount struct { + BaseModel + Email string `gorm:"not null" json:"email"` + URL string `gorm:"not null" json:"url"` + PrivateKey string `gorm:"not null" json:"-"` + Type string `gorm:"not null;default:letsencrypt" json:"type"` + EabKid string `gorm:"default:null;" json:"eabKid"` + EabHmacKey string `gorm:"default:null" json:"eabHmacKey"` + KeyType string `gorm:"not null;default:2048" json:"keyType"` +} + +func (w WebsiteAcmeAccount) TableName() string { + return "website_acme_accounts" +} diff --git a/agent/app/model/website_ca.go b/agent/app/model/website_ca.go new file mode 100644 index 000000000..74b029637 --- /dev/null +++ b/agent/app/model/website_ca.go @@ -0,0 +1,9 @@ +package model + +type WebsiteCA struct { + BaseModel + CSR string `gorm:"not null;" json:"csr"` + Name string `gorm:"not null;" json:"name"` + PrivateKey string `gorm:"not null" json:"privateKey"` + KeyType string `gorm:"not null;default:2048" json:"keyType"` +} diff --git a/agent/app/model/website_dns_account.go b/agent/app/model/website_dns_account.go new file mode 100644 index 000000000..de7ae8dee --- /dev/null +++ b/agent/app/model/website_dns_account.go @@ -0,0 +1,12 @@ +package model + +type WebsiteDnsAccount struct { + BaseModel + Name string `gorm:"type:varchar(64);not null" json:"name"` + Type string `gorm:"type:varchar(64);not null" json:"type"` + Authorization string `gorm:"type:varchar(256);not null" json:"-"` +} + +func (w WebsiteDnsAccount) TableName() string { + return "website_dns_accounts" +} diff --git a/agent/app/model/website_domain.go b/agent/app/model/website_domain.go new file mode 100644 index 000000000..e6fb77e93 --- /dev/null +++ b/agent/app/model/website_domain.go @@ -0,0 +1,12 @@ +package model + +type WebsiteDomain struct { + BaseModel + WebsiteID uint `gorm:"column:website_id;type:varchar(64);not null;" json:"websiteId"` + Domain string `gorm:"type:varchar(256);not null" json:"domain"` + Port int `gorm:"type:integer" json:"port"` +} + +func (w WebsiteDomain) TableName() string { + return "website_domains" +} diff --git a/agent/app/model/website_ssl.go b/agent/app/model/website_ssl.go new file mode 100644 index 000000000..da5281dc9 --- /dev/null +++ b/agent/app/model/website_ssl.go @@ -0,0 +1,51 @@ +package model + +import ( + "fmt" + "path" + "time" + + "github.com/1Panel-dev/1Panel/agent/constant" +) + +type WebsiteSSL struct { + BaseModel + PrimaryDomain string `json:"primaryDomain"` + PrivateKey string `json:"privateKey"` + Pem string `json:"pem"` + Domains string `json:"domains"` + CertURL string `json:"certURL"` + Type string `json:"type"` + Provider string `json:"provider"` + Organization string `json:"organization"` + DnsAccountID uint `json:"dnsAccountId"` + AcmeAccountID uint `gorm:"column:acme_account_id" json:"acmeAccountId" ` + CaID uint `json:"caId"` + AutoRenew bool `json:"autoRenew"` + ExpireDate time.Time `json:"expireDate"` + StartDate time.Time `json:"startDate"` + Status string `json:"status"` + Message string `json:"message"` + KeyType string `json:"keyType"` + PushDir bool `json:"pushDir"` + Dir string `json:"dir"` + Description string `json:"description"` + SkipDNS bool `json:"skipDNS"` + Nameserver1 string `json:"nameserver1"` + Nameserver2 string `json:"nameserver2"` + DisableCNAME bool `json:"disableCNAME"` + ExecShell bool `json:"execShell"` + Shell string `json:"shell"` + + AcmeAccount WebsiteAcmeAccount `json:"acmeAccount" gorm:"-:migration"` + DnsAccount WebsiteDnsAccount `json:"dnsAccount" gorm:"-:migration"` + Websites []Website `json:"websites" gorm:"-:migration"` +} + +func (w WebsiteSSL) TableName() string { + return "website_ssls" +} + +func (w WebsiteSSL) GetLogPath() string { + return path.Join(constant.SSLLogDir, fmt.Sprintf("%s-ssl-%d.log", w.PrimaryDomain, w.ID)) +} diff --git a/agent/app/repo/app.go b/agent/app/repo/app.go new file mode 100644 index 000000000..8ab5729f2 --- /dev/null +++ b/agent/app/repo/app.go @@ -0,0 +1,123 @@ +package repo + +import ( + "context" + + "github.com/1Panel-dev/1Panel/agent/app/model" + "gorm.io/gorm" + "gorm.io/gorm/clause" +) + +type AppRepo struct { +} + +type IAppRepo interface { + WithKey(key string) DBOption + WithType(typeStr string) DBOption + OrderByRecommend() DBOption + GetRecommend() DBOption + WithResource(resource string) DBOption + WithLikeName(name string) DBOption + Page(page, size int, opts ...DBOption) (int64, []model.App, error) + GetFirst(opts ...DBOption) (model.App, error) + GetBy(opts ...DBOption) ([]model.App, error) + BatchCreate(ctx context.Context, apps []model.App) error + GetByKey(ctx context.Context, key string) (model.App, error) + Create(ctx context.Context, app *model.App) error + Save(ctx context.Context, app *model.App) error + BatchDelete(ctx context.Context, apps []model.App) error +} + +func NewIAppRepo() IAppRepo { + return &AppRepo{} +} + +func (a AppRepo) WithLikeName(name string) DBOption { + return func(g *gorm.DB) *gorm.DB { + if len(name) == 0 { + return g + } + return g.Where("name like ? or short_desc_zh like ? or short_desc_en like ?", "%"+name+"%", "%"+name+"%", "%"+name+"%") + } +} + +func (a AppRepo) WithKey(key string) DBOption { + return func(db *gorm.DB) *gorm.DB { + return db.Where("key = ?", key) + } +} + +func (a AppRepo) WithType(typeStr string) DBOption { + return func(g *gorm.DB) *gorm.DB { + return g.Where("type = ?", typeStr) + } +} + +func (a AppRepo) OrderByRecommend() DBOption { + return func(g *gorm.DB) *gorm.DB { + return g.Order("recommend asc") + } +} + +func (a AppRepo) GetRecommend() DBOption { + return func(g *gorm.DB) *gorm.DB { + return g.Where("recommend < 9999") + } +} + +func (a AppRepo) WithResource(resource string) DBOption { + return func(g *gorm.DB) *gorm.DB { + return g.Where("resource = ?", resource) + } +} + +func (a AppRepo) Page(page, size int, opts ...DBOption) (int64, []model.App, error) { + var apps []model.App + db := getDb(opts...).Model(&model.App{}) + count := int64(0) + db = db.Count(&count) + err := db.Limit(size).Offset(size * (page - 1)).Preload("AppTags").Find(&apps).Error + return count, apps, err +} + +func (a AppRepo) GetFirst(opts ...DBOption) (model.App, error) { + var app model.App + db := getDb(opts...).Model(&model.App{}) + if err := db.Preload("AppTags").First(&app).Error; err != nil { + return app, err + } + return app, nil +} + +func (a AppRepo) GetBy(opts ...DBOption) ([]model.App, error) { + var apps []model.App + db := getDb(opts...).Model(&model.App{}) + if err := db.Preload("Details").Preload("AppTags").Find(&apps).Error; err != nil { + return apps, err + } + return apps, nil +} + +func (a AppRepo) BatchCreate(ctx context.Context, apps []model.App) error { + return getTx(ctx).Omit(clause.Associations).Create(&apps).Error +} + +func (a AppRepo) GetByKey(ctx context.Context, key string) (model.App, error) { + var app model.App + if err := getTx(ctx).Where("key = ?", key).First(&app).Error; err != nil { + return app, err + } + return app, nil +} + +func (a AppRepo) Create(ctx context.Context, app *model.App) error { + return getTx(ctx).Omit(clause.Associations).Create(app).Error +} + +func (a AppRepo) Save(ctx context.Context, app *model.App) error { + return getTx(ctx).Omit(clause.Associations).Save(app).Error +} + +func (a AppRepo) BatchDelete(ctx context.Context, apps []model.App) error { + return getTx(ctx).Omit(clause.Associations).Delete(&apps).Error +} diff --git a/agent/app/repo/app_detail.go b/agent/app/repo/app_detail.go new file mode 100644 index 000000000..de1267d95 --- /dev/null +++ b/agent/app/repo/app_detail.go @@ -0,0 +1,83 @@ +package repo + +import ( + "context" + + "github.com/1Panel-dev/1Panel/agent/app/model" + "gorm.io/gorm" + "gorm.io/gorm/clause" +) + +type AppDetailRepo struct { +} + +type IAppDetailRepo interface { + WithVersion(version string) DBOption + WithAppId(id uint) DBOption + WithIgnored() DBOption + GetFirst(opts ...DBOption) (model.AppDetail, error) + Update(ctx context.Context, detail model.AppDetail) error + BatchCreate(ctx context.Context, details []model.AppDetail) error + DeleteByAppIds(ctx context.Context, appIds []uint) error + GetBy(opts ...DBOption) ([]model.AppDetail, error) + BatchUpdateBy(maps map[string]interface{}, opts ...DBOption) error + BatchDelete(ctx context.Context, appDetails []model.AppDetail) error +} + +func NewIAppDetailRepo() IAppDetailRepo { + return &AppDetailRepo{} +} + +func (a AppDetailRepo) WithVersion(version string) DBOption { + return func(g *gorm.DB) *gorm.DB { + return g.Where("version = ?", version) + } +} + +func (a AppDetailRepo) WithAppId(id uint) DBOption { + return func(g *gorm.DB) *gorm.DB { + return g.Where("app_id = ?", id) + } +} + +func (a AppDetailRepo) WithIgnored() DBOption { + return func(g *gorm.DB) *gorm.DB { + return g.Where("ignore_upgrade = 1") + } +} + +func (a AppDetailRepo) GetFirst(opts ...DBOption) (model.AppDetail, error) { + var detail model.AppDetail + err := getDb(opts...).Model(&model.AppDetail{}).Find(&detail).Error + return detail, err +} + +func (a AppDetailRepo) Update(ctx context.Context, detail model.AppDetail) error { + return getTx(ctx).Save(&detail).Error +} + +func (a AppDetailRepo) BatchCreate(ctx context.Context, details []model.AppDetail) error { + return getTx(ctx).Model(&model.AppDetail{}).Create(&details).Error +} + +func (a AppDetailRepo) DeleteByAppIds(ctx context.Context, appIds []uint) error { + return getTx(ctx).Where("app_id in (?)", appIds).Delete(&model.AppDetail{}).Error +} + +func (a AppDetailRepo) GetBy(opts ...DBOption) ([]model.AppDetail, error) { + var details []model.AppDetail + err := getDb(opts...).Find(&details).Error + return details, err +} + +func (a AppDetailRepo) BatchUpdateBy(maps map[string]interface{}, opts ...DBOption) error { + db := getDb(opts...).Model(&model.AppDetail{}) + if len(opts) == 0 { + db = db.Where("1=1") + } + return db.Updates(&maps).Error +} + +func (a AppDetailRepo) BatchDelete(ctx context.Context, appDetails []model.AppDetail) error { + return getTx(ctx).Omit(clause.Associations).Delete(&appDetails).Error +} diff --git a/agent/app/repo/app_install.go b/agent/app/repo/app_install.go new file mode 100644 index 000000000..7176e6a7f --- /dev/null +++ b/agent/app/repo/app_install.go @@ -0,0 +1,241 @@ +package repo + +import ( + "context" + "encoding/json" + + "github.com/1Panel-dev/1Panel/agent/constant" + + "gorm.io/gorm/clause" + + "github.com/1Panel-dev/1Panel/agent/app/model" + "github.com/1Panel-dev/1Panel/agent/global" + "gorm.io/gorm" +) + +type AppInstallRepo struct{} + +type IAppInstallRepo interface { + WithDetailIdsIn(detailIds []uint) DBOption + WithDetailIdNotIn(detailIds []uint) DBOption + WithAppId(appId uint) DBOption + WithAppIdsIn(appIds []uint) DBOption + WithStatus(status string) DBOption + WithServiceName(serviceName string) DBOption + WithContainerName(containerName string) DBOption + WithPort(port int) DBOption + WithIdNotInWebsite() DBOption + WithIDNotIs(id uint) DBOption + ListBy(opts ...DBOption) ([]model.AppInstall, error) + GetFirst(opts ...DBOption) (model.AppInstall, error) + Create(ctx context.Context, install *model.AppInstall) error + Save(ctx context.Context, install *model.AppInstall) error + DeleteBy(opts ...DBOption) error + Delete(ctx context.Context, install model.AppInstall) error + Page(page, size int, opts ...DBOption) (int64, []model.AppInstall, error) + BatchUpdateBy(maps map[string]interface{}, opts ...DBOption) error + LoadBaseInfo(key string, name string) (*RootInfo, error) + GetFirstByCtx(ctx context.Context, opts ...DBOption) (model.AppInstall, error) +} + +func NewIAppInstallRepo() IAppInstallRepo { + return &AppInstallRepo{} +} + +func (a *AppInstallRepo) WithDetailIdsIn(detailIds []uint) DBOption { + return func(g *gorm.DB) *gorm.DB { + return g.Where("app_detail_id in (?)", detailIds) + } +} + +func (a *AppInstallRepo) WithDetailIdNotIn(detailIds []uint) DBOption { + return func(g *gorm.DB) *gorm.DB { + return g.Where("app_detail_id not in (?)", detailIds) + } +} + +func (a *AppInstallRepo) WithAppId(appId uint) DBOption { + return func(g *gorm.DB) *gorm.DB { + return g.Where("app_id = ?", appId) + } +} + +func (a *AppInstallRepo) WithIDNotIs(id uint) DBOption { + return func(g *gorm.DB) *gorm.DB { + return g.Where("id != ?", id) + } +} + +func (a *AppInstallRepo) WithAppIdsIn(appIds []uint) DBOption { + return func(g *gorm.DB) *gorm.DB { + return g.Where("app_id in (?)", appIds) + } +} + +func (a *AppInstallRepo) WithStatus(status string) DBOption { + return func(g *gorm.DB) *gorm.DB { + return g.Where("status = ?", status) + } +} + +func (a *AppInstallRepo) WithServiceName(serviceName string) DBOption { + return func(db *gorm.DB) *gorm.DB { + return db.Where("service_name = ?", serviceName) + } +} + +func (a *AppInstallRepo) WithContainerName(containerName string) DBOption { + return func(db *gorm.DB) *gorm.DB { + return db.Where("container_name = ?", containerName) + } +} + +func (a *AppInstallRepo) WithPort(port int) DBOption { + return func(db *gorm.DB) *gorm.DB { + return db.Where("https_port = ? or http_port = ?", port, port) + } +} + +func (a *AppInstallRepo) WithIdNotInWebsite() DBOption { + return func(db *gorm.DB) *gorm.DB { + return db.Where("id not in (select app_install_id from websites)") + } +} + +func (a *AppInstallRepo) ListBy(opts ...DBOption) ([]model.AppInstall, error) { + var install []model.AppInstall + db := getDb(opts...).Model(&model.AppInstall{}) + err := db.Preload("App").Find(&install).Error + return install, err +} + +func (a *AppInstallRepo) GetFirst(opts ...DBOption) (model.AppInstall, error) { + var install model.AppInstall + db := getDb(opts...).Model(&model.AppInstall{}) + err := db.Preload("App").First(&install).Error + return install, err +} + +func (a *AppInstallRepo) GetFirstByCtx(ctx context.Context, opts ...DBOption) (model.AppInstall, error) { + var install model.AppInstall + db := getTx(ctx, opts...).Model(&model.AppInstall{}) + err := db.Preload("App").First(&install).Error + return install, err +} + +func (a *AppInstallRepo) Create(ctx context.Context, install *model.AppInstall) error { + db := getTx(ctx).Model(&model.AppInstall{}) + return db.Omit(clause.Associations).Create(&install).Error +} + +func (a *AppInstallRepo) Save(ctx context.Context, install *model.AppInstall) error { + return getTx(ctx).Save(&install).Error +} + +func (a *AppInstallRepo) DeleteBy(opts ...DBOption) error { + return getDb(opts...).Delete(&model.AppInstall{}).Error +} + +func (a *AppInstallRepo) Delete(ctx context.Context, install model.AppInstall) error { + db := getTx(ctx).Model(&model.AppInstall{}) + return db.Delete(&install).Error +} + +func (a *AppInstallRepo) Page(page, size int, opts ...DBOption) (int64, []model.AppInstall, error) { + var apps []model.AppInstall + db := getDb(opts...).Model(&model.AppInstall{}) + count := int64(0) + db = db.Count(&count) + err := db.Limit(size).Offset(size * (page - 1)).Preload("App").Find(&apps).Error + return count, apps, err +} + +func (a *AppInstallRepo) BatchUpdateBy(maps map[string]interface{}, opts ...DBOption) error { + db := getDb(opts...).Model(&model.AppInstall{}) + if len(opts) == 0 { + db = db.Where("1=1") + } + return db.Updates(&maps).Error +} + +type RootInfo struct { + ID uint `json:"id"` + Name string `json:"name"` + Status string `json:"status"` + Port int64 `json:"port"` + HttpsPort int64 `json:"httpsPort"` + UserName string `json:"userName"` + Password string `json:"password"` + UserPassword string `json:"userPassword"` + ContainerName string `json:"containerName"` + ServiceName string `json:"serviceName"` + Param string `json:"param"` + Env string `json:"env"` + Key string `json:"key"` + Version string `json:"version"` + AppPath string `json:"app_path"` +} + +func (a *AppInstallRepo) LoadBaseInfo(key string, name string) (*RootInfo, error) { + var ( + app model.App + appInstall model.AppInstall + info RootInfo + ) + if err := global.DB.Where("key = ?", key).First(&app).Error; err != nil { + return nil, err + } + if len(name) == 0 { + if err := global.DB.Where("app_id = ?", app.ID).First(&appInstall).Error; err != nil { + return nil, err + } + } else { + if err := global.DB.Where("app_id = ? AND name = ?", app.ID, name).First(&appInstall).Error; err != nil { + return nil, err + } + } + envMap := make(map[string]interface{}) + if err := json.Unmarshal([]byte(appInstall.Env), &envMap); err != nil { + return nil, err + } + switch app.Key { + case "mysql", "mariadb": + password, ok := envMap["PANEL_DB_ROOT_PASSWORD"].(string) + if ok { + info.Password = password + } + case "redis": + password, ok := envMap["PANEL_REDIS_ROOT_PASSWORD"].(string) + if ok { + info.Password = password + } + case "mongodb", constant.AppPostgresql: + user, ok := envMap["PANEL_DB_ROOT_USER"].(string) + if ok { + info.UserName = user + } + password, ok := envMap["PANEL_DB_ROOT_PASSWORD"].(string) + if ok { + info.Password = password + } + } + + userPassword, ok := envMap["PANEL_DB_USER_PASSWORD"].(string) + if ok { + info.UserPassword = userPassword + } + info.Port = int64(appInstall.HttpPort) + info.HttpsPort = int64(appInstall.HttpsPort) + info.ID = appInstall.ID + info.ServiceName = appInstall.ServiceName + info.ContainerName = appInstall.ContainerName + info.Name = appInstall.Name + info.Env = appInstall.Env + info.Param = appInstall.Param + info.Version = appInstall.Version + info.Key = app.Key + appInstall.App = app + info.AppPath = appInstall.GetAppPath() + info.Status = appInstall.Status + return &info, nil +} diff --git a/agent/app/repo/app_install_resource.go b/agent/app/repo/app_install_resource.go new file mode 100644 index 000000000..52850fa43 --- /dev/null +++ b/agent/app/repo/app_install_resource.go @@ -0,0 +1,82 @@ +package repo + +import ( + "context" + + "github.com/1Panel-dev/1Panel/agent/app/model" + "github.com/1Panel-dev/1Panel/agent/global" + "gorm.io/gorm" +) + +type AppInstallResourceRpo struct { +} + +type IAppInstallResourceRpo interface { + WithAppInstallId(appInstallId uint) DBOption + WithLinkId(linkId uint) DBOption + WithResourceId(resourceId uint) DBOption + GetBy(opts ...DBOption) ([]model.AppInstallResource, error) + GetFirst(opts ...DBOption) (model.AppInstallResource, error) + Create(ctx context.Context, resource *model.AppInstallResource) error + DeleteBy(ctx context.Context, opts ...DBOption) error + BatchUpdateBy(maps map[string]interface{}, opts ...DBOption) error +} + +func NewIAppInstallResourceRpo() IAppInstallResourceRpo { + return &AppInstallResourceRpo{} +} + +func (a AppInstallResourceRpo) WithAppInstallId(appInstallId uint) DBOption { + return func(db *gorm.DB) *gorm.DB { + return db.Where("app_install_id = ?", appInstallId) + } +} + +func (a AppInstallResourceRpo) WithLinkId(linkId uint) DBOption { + return func(db *gorm.DB) *gorm.DB { + return db.Where("link_id = ?", linkId) + } +} + +func (a AppInstallResourceRpo) WithResourceId(resourceId uint) DBOption { + return func(db *gorm.DB) *gorm.DB { + return db.Where("resource_id = ?", resourceId) + } +} + +func (a AppInstallResourceRpo) GetBy(opts ...DBOption) ([]model.AppInstallResource, error) { + db := global.DB.Model(&model.AppInstallResource{}) + var resources []model.AppInstallResource + for _, opt := range opts { + db = opt(db) + } + err := db.Find(&resources).Error + return resources, err +} + +func (a AppInstallResourceRpo) GetFirst(opts ...DBOption) (model.AppInstallResource, error) { + db := global.DB.Model(&model.AppInstallResource{}) + var resources model.AppInstallResource + for _, opt := range opts { + db = opt(db) + } + err := db.First(&resources).Error + return resources, err +} + +func (a AppInstallResourceRpo) Create(ctx context.Context, resource *model.AppInstallResource) error { + db := getTx(ctx).Model(&model.AppInstallResource{}) + return db.Create(&resource).Error +} + +func (a AppInstallResourceRpo) DeleteBy(ctx context.Context, opts ...DBOption) error { + return getTx(ctx, opts...).Delete(&model.AppInstallResource{}).Error +} + +func (a *AppInstallResourceRpo) BatchUpdateBy(maps map[string]interface{}, opts ...DBOption) error { + db := getDb(opts...).Model(&model.AppInstallResource{}) + if len(opts) == 0 { + db = db.Where("1=1") + } + return db.Updates(&maps).Error +} diff --git a/agent/app/repo/app_tag.go b/agent/app/repo/app_tag.go new file mode 100644 index 000000000..a32ef0097 --- /dev/null +++ b/agent/app/repo/app_tag.go @@ -0,0 +1,50 @@ +package repo + +import ( + "context" + + "github.com/1Panel-dev/1Panel/agent/app/model" +) + +type AppTagRepo struct { +} + +type IAppTagRepo interface { + BatchCreate(ctx context.Context, tags []*model.AppTag) error + DeleteByAppIds(ctx context.Context, appIds []uint) error + DeleteAll(ctx context.Context) error + GetByAppId(appId uint) ([]model.AppTag, error) + GetByTagIds(tagIds []uint) ([]model.AppTag, error) +} + +func NewIAppTagRepo() IAppTagRepo { + return &AppTagRepo{} +} + +func (a AppTagRepo) BatchCreate(ctx context.Context, tags []*model.AppTag) error { + return getTx(ctx).Create(&tags).Error +} + +func (a AppTagRepo) DeleteByAppIds(ctx context.Context, appIds []uint) error { + return getTx(ctx).Where("app_id in (?)", appIds).Delete(&model.AppTag{}).Error +} + +func (a AppTagRepo) DeleteAll(ctx context.Context) error { + return getTx(ctx).Where("1 = 1").Delete(&model.AppTag{}).Error +} + +func (a AppTagRepo) GetByAppId(appId uint) ([]model.AppTag, error) { + var appTags []model.AppTag + if err := getDb().Where("app_id = ?", appId).Find(&appTags).Error; err != nil { + return nil, err + } + return appTags, nil +} + +func (a AppTagRepo) GetByTagIds(tagIds []uint) ([]model.AppTag, error) { + var appTags []model.AppTag + if err := getDb().Where("tag_id in (?)", tagIds).Find(&appTags).Error; err != nil { + return nil, err + } + return appTags, nil +} diff --git a/agent/app/repo/backup.go b/agent/app/repo/backup.go new file mode 100644 index 000000000..33ab0c6dd --- /dev/null +++ b/agent/app/repo/backup.go @@ -0,0 +1,135 @@ +package repo + +import ( + "context" + + "github.com/1Panel-dev/1Panel/agent/app/model" + "github.com/1Panel-dev/1Panel/agent/global" + "gorm.io/gorm" +) + +type BackupRepo struct{} + +type IBackupRepo interface { + Get(opts ...DBOption) (model.BackupAccount, error) + ListRecord(opts ...DBOption) ([]model.BackupRecord, error) + PageRecord(page, size int, opts ...DBOption) (int64, []model.BackupRecord, error) + List(opts ...DBOption) ([]model.BackupAccount, error) + Create(backup *model.BackupAccount) error + CreateRecord(record *model.BackupRecord) error + Update(id uint, vars map[string]interface{}) error + Delete(opts ...DBOption) error + DeleteRecord(ctx context.Context, opts ...DBOption) error + UpdateRecord(record *model.BackupRecord) error + WithByDetailName(detailName string) DBOption + WithByFileName(fileName string) DBOption + WithByType(backupType string) DBOption + WithByCronID(cronjobID uint) DBOption +} + +func NewIBackupRepo() IBackupRepo { + return &BackupRepo{} +} + +func (u *BackupRepo) Get(opts ...DBOption) (model.BackupAccount, error) { + var backup model.BackupAccount + db := global.DB + for _, opt := range opts { + db = opt(db) + } + err := db.First(&backup).Error + return backup, err +} + +func (u *BackupRepo) ListRecord(opts ...DBOption) ([]model.BackupRecord, error) { + var users []model.BackupRecord + db := global.DB.Model(&model.BackupRecord{}) + for _, opt := range opts { + db = opt(db) + } + err := db.Find(&users).Error + return users, err +} + +func (u *BackupRepo) PageRecord(page, size int, opts ...DBOption) (int64, []model.BackupRecord, error) { + var users []model.BackupRecord + db := global.DB.Model(&model.BackupRecord{}) + for _, opt := range opts { + db = opt(db) + } + count := int64(0) + db = db.Count(&count) + err := db.Limit(size).Offset(size * (page - 1)).Find(&users).Error + return count, users, err +} + +func (u *BackupRepo) WithByDetailName(detailName string) DBOption { + return func(g *gorm.DB) *gorm.DB { + if len(detailName) == 0 { + return g + } + return g.Where("detail_name = ?", detailName) + } +} + +func (u *BackupRepo) WithByFileName(fileName string) DBOption { + return func(g *gorm.DB) *gorm.DB { + if len(fileName) == 0 { + return g + } + return g.Where("file_name = ?", fileName) + } +} + +func (u *BackupRepo) WithByType(backupType string) DBOption { + return func(g *gorm.DB) *gorm.DB { + if len(backupType) == 0 { + return g + } + return g.Where("type = ?", backupType) + } +} + +func (u *BackupRepo) List(opts ...DBOption) ([]model.BackupAccount, error) { + var ops []model.BackupAccount + db := global.DB.Model(&model.BackupAccount{}) + for _, opt := range opts { + db = opt(db) + } + err := db.Find(&ops).Error + return ops, err +} + +func (u *BackupRepo) Create(backup *model.BackupAccount) error { + return global.DB.Create(backup).Error +} + +func (u *BackupRepo) CreateRecord(record *model.BackupRecord) error { + return global.DB.Create(record).Error +} + +func (u *BackupRepo) UpdateRecord(record *model.BackupRecord) error { + return global.DB.Save(record).Error +} + +func (u *BackupRepo) Update(id uint, vars map[string]interface{}) error { + return global.DB.Model(&model.BackupAccount{}).Where("id = ?", id).Updates(vars).Error +} + +func (u *BackupRepo) Delete(opts ...DBOption) error { + db := global.DB + for _, opt := range opts { + db = opt(db) + } + return db.Delete(&model.BackupAccount{}).Error +} + +func (u *BackupRepo) DeleteRecord(ctx context.Context, opts ...DBOption) error { + return getTx(ctx, opts...).Delete(&model.BackupRecord{}).Error +} + +func (u *BackupRepo) WithByCronID(cronjobID uint) DBOption { + return func(g *gorm.DB) *gorm.DB { + return g.Where("cronjob_id = ?", cronjobID) + } +} diff --git a/agent/app/repo/clam.go b/agent/app/repo/clam.go new file mode 100644 index 000000000..d1fdcecda --- /dev/null +++ b/agent/app/repo/clam.go @@ -0,0 +1,69 @@ +package repo + +import ( + "github.com/1Panel-dev/1Panel/agent/app/model" + "github.com/1Panel-dev/1Panel/agent/global" +) + +type ClamRepo struct{} + +type IClamRepo interface { + Page(limit, offset int, opts ...DBOption) (int64, []model.Clam, error) + Create(clam *model.Clam) error + Update(id uint, vars map[string]interface{}) error + Delete(opts ...DBOption) error + Get(opts ...DBOption) (model.Clam, error) + List(opts ...DBOption) ([]model.Clam, error) +} + +func NewIClamRepo() IClamRepo { + return &ClamRepo{} +} + +func (u *ClamRepo) Get(opts ...DBOption) (model.Clam, error) { + var clam model.Clam + db := global.DB + for _, opt := range opts { + db = opt(db) + } + err := db.First(&clam).Error + return clam, err +} + +func (u *ClamRepo) List(opts ...DBOption) ([]model.Clam, error) { + var clam []model.Clam + db := global.DB + for _, opt := range opts { + db = opt(db) + } + err := db.Find(&clam).Error + return clam, err +} + +func (u *ClamRepo) Page(page, size int, opts ...DBOption) (int64, []model.Clam, error) { + var users []model.Clam + db := global.DB.Model(&model.Clam{}) + for _, opt := range opts { + db = opt(db) + } + count := int64(0) + db = db.Count(&count) + err := db.Limit(size).Offset(size * (page - 1)).Find(&users).Error + return count, users, err +} + +func (u *ClamRepo) Create(clam *model.Clam) error { + return global.DB.Create(clam).Error +} + +func (u *ClamRepo) Update(id uint, vars map[string]interface{}) error { + return global.DB.Model(&model.Clam{}).Where("id = ?", id).Updates(vars).Error +} + +func (u *ClamRepo) Delete(opts ...DBOption) error { + db := global.DB + for _, opt := range opts { + db = opt(db) + } + return db.Delete(&model.Clam{}).Error +} diff --git a/agent/app/repo/command.go b/agent/app/repo/command.go new file mode 100644 index 000000000..e2e9c7124 --- /dev/null +++ b/agent/app/repo/command.go @@ -0,0 +1,141 @@ +package repo + +import ( + "github.com/1Panel-dev/1Panel/agent/app/model" + "github.com/1Panel-dev/1Panel/agent/global" + "gorm.io/gorm" +) + +type CommandRepo struct{} + +type ICommandRepo interface { + GetList(opts ...DBOption) ([]model.Command, error) + Page(limit, offset int, opts ...DBOption) (int64, []model.Command, error) + WithByInfo(info string) DBOption + Create(command *model.Command) error + Update(id uint, vars map[string]interface{}) error + Delete(opts ...DBOption) error + Get(opts ...DBOption) (model.Command, error) + WithLikeName(name string) DBOption + + PageRedis(limit, offset int, opts ...DBOption) (int64, []model.RedisCommand, error) + GetRedis(opts ...DBOption) (model.RedisCommand, error) + GetRedisList(opts ...DBOption) ([]model.RedisCommand, error) + SaveRedis(command *model.RedisCommand) error + DeleteRedis(opts ...DBOption) error +} + +func NewICommandRepo() ICommandRepo { + return &CommandRepo{} +} + +func (u *CommandRepo) Get(opts ...DBOption) (model.Command, error) { + var command model.Command + db := global.DB + for _, opt := range opts { + db = opt(db) + } + err := db.First(&command).Error + return command, err +} + +func (u *CommandRepo) GetRedis(opts ...DBOption) (model.RedisCommand, error) { + var command model.RedisCommand + db := global.DB + for _, opt := range opts { + db = opt(db) + } + err := db.First(&command).Error + return command, err +} + +func (u *CommandRepo) Page(page, size int, opts ...DBOption) (int64, []model.Command, error) { + var users []model.Command + db := global.DB.Model(&model.Command{}) + for _, opt := range opts { + db = opt(db) + } + count := int64(0) + db = db.Count(&count) + err := db.Limit(size).Offset(size * (page - 1)).Find(&users).Error + return count, users, err +} + +func (u *CommandRepo) PageRedis(page, size int, opts ...DBOption) (int64, []model.RedisCommand, error) { + var users []model.RedisCommand + db := global.DB.Model(&model.RedisCommand{}) + for _, opt := range opts { + db = opt(db) + } + count := int64(0) + db = db.Count(&count) + err := db.Limit(size).Offset(size * (page - 1)).Find(&users).Error + return count, users, err +} + +func (u *CommandRepo) GetList(opts ...DBOption) ([]model.Command, error) { + var commands []model.Command + db := global.DB.Model(&model.Command{}) + for _, opt := range opts { + db = opt(db) + } + err := db.Find(&commands).Error + return commands, err +} + +func (u *CommandRepo) GetRedisList(opts ...DBOption) ([]model.RedisCommand, error) { + var commands []model.RedisCommand + db := global.DB.Model(&model.RedisCommand{}) + for _, opt := range opts { + db = opt(db) + } + err := db.Find(&commands).Error + return commands, err +} + +func (c *CommandRepo) WithByInfo(info string) DBOption { + return func(g *gorm.DB) *gorm.DB { + if len(info) == 0 { + return g + } + infoStr := "%" + info + "%" + return g.Where("name LIKE ? OR addr LIKE ?", infoStr, infoStr) + } +} + +func (u *CommandRepo) Create(command *model.Command) error { + return global.DB.Create(command).Error +} + +func (u *CommandRepo) SaveRedis(command *model.RedisCommand) error { + return global.DB.Save(command).Error +} + +func (u *CommandRepo) Update(id uint, vars map[string]interface{}) error { + return global.DB.Model(&model.Command{}).Where("id = ?", id).Updates(vars).Error +} + +func (u *CommandRepo) Delete(opts ...DBOption) error { + db := global.DB + for _, opt := range opts { + db = opt(db) + } + return db.Delete(&model.Command{}).Error +} + +func (u *CommandRepo) DeleteRedis(opts ...DBOption) error { + db := global.DB + for _, opt := range opts { + db = opt(db) + } + return db.Delete(&model.RedisCommand{}).Error +} + +func (a CommandRepo) WithLikeName(name string) DBOption { + return func(g *gorm.DB) *gorm.DB { + if len(name) == 0 { + return g + } + return g.Where("name like ? or command like ?", "%"+name+"%", "%"+name+"%") + } +} diff --git a/agent/app/repo/common.go b/agent/app/repo/common.go new file mode 100644 index 000000000..3be569252 --- /dev/null +++ b/agent/app/repo/common.go @@ -0,0 +1,149 @@ +package repo + +import ( + "context" + "fmt" + "time" + + "github.com/1Panel-dev/1Panel/agent/constant" + "github.com/1Panel-dev/1Panel/agent/global" + "gorm.io/gorm" +) + +type DBOption func(*gorm.DB) *gorm.DB + +type ICommonRepo interface { + WithByID(id uint) DBOption + WithByName(name string) DBOption + WithByType(tp string) DBOption + WithOrderBy(orderStr string) DBOption + WithOrderRuleBy(orderBy, order string) DBOption + WithByGroupID(groupID uint) DBOption + WithLikeName(name string) DBOption + WithIdsIn(ids []uint) DBOption + WithByDate(startTime, endTime time.Time) DBOption + WithByStartDate(startTime time.Time) DBOption + WithByStatus(status string) DBOption + WithByFrom(from string) DBOption +} + +type CommonRepo struct{} + +func NewCommonRepo() ICommonRepo { + return &CommonRepo{} +} + +func (c *CommonRepo) WithByID(id uint) DBOption { + return func(g *gorm.DB) *gorm.DB { + return g.Where("id = ?", id) + } +} + +func (c *CommonRepo) WithByName(name string) DBOption { + return func(g *gorm.DB) *gorm.DB { + return g.Where("name = ?", name) + } +} + +func (c *CommonRepo) WithByDate(startTime, endTime time.Time) DBOption { + return func(g *gorm.DB) *gorm.DB { + return g.Where("start_time > ? AND start_time < ?", startTime, endTime) + } +} + +func (c *CommonRepo) WithByStartDate(startTime time.Time) DBOption { + return func(g *gorm.DB) *gorm.DB { + return g.Where("start_time < ?", startTime) + } +} + +func (c *CommonRepo) WithByType(tp string) DBOption { + return func(g *gorm.DB) *gorm.DB { + return g.Where("type = ?", tp) + } +} + +func (c *CommonRepo) WithByGroupID(groupID uint) DBOption { + return func(g *gorm.DB) *gorm.DB { + if groupID == 0 { + return g + } + return g.Where("group_id = ?", groupID) + } +} + +func (c *CommonRepo) WithByStatus(status string) DBOption { + return func(g *gorm.DB) *gorm.DB { + if len(status) == 0 { + return g + } + return g.Where("status = ?", status) + } +} + +func (c *CommonRepo) WithByFrom(from string) DBOption { + return func(g *gorm.DB) *gorm.DB { + return g.Where("`from` = ?", from) + } +} + +func (c *CommonRepo) WithLikeName(name string) DBOption { + return func(g *gorm.DB) *gorm.DB { + if len(name) == 0 { + return g + } + return g.Where("name like ?", "%"+name+"%") + } +} + +func (c *CommonRepo) WithOrderBy(orderStr string) DBOption { + return func(g *gorm.DB) *gorm.DB { + return g.Order(orderStr) + } +} + +func (c *CommonRepo) WithOrderRuleBy(orderBy, order string) DBOption { + switch order { + case constant.OrderDesc: + order = "desc" + case constant.OrderAsc: + order = "asc" + default: + orderBy = "created_at" + order = "desc" + } + return func(g *gorm.DB) *gorm.DB { + return g.Order(fmt.Sprintf("%s %s", orderBy, order)) + } +} + +func (c *CommonRepo) WithIdsIn(ids []uint) DBOption { + return func(g *gorm.DB) *gorm.DB { + return g.Where("id in (?)", ids) + } +} + +func (c *CommonRepo) WithIdsNotIn(ids []uint) DBOption { + return func(g *gorm.DB) *gorm.DB { + return g.Where("id not in (?)", ids) + } +} + +func getTx(ctx context.Context, opts ...DBOption) *gorm.DB { + tx, ok := ctx.Value(constant.DB).(*gorm.DB) + if ok { + for _, opt := range opts { + tx = opt(tx) + } + return tx + } + return getDb(opts...) +} + +func getDb(opts ...DBOption) *gorm.DB { + db := global.DB + for _, opt := range opts { + db = opt(db) + } + return db +} diff --git a/agent/app/repo/compose_template.go b/agent/app/repo/compose_template.go new file mode 100644 index 000000000..8233e162f --- /dev/null +++ b/agent/app/repo/compose_template.go @@ -0,0 +1,104 @@ +package repo + +import ( + "github.com/1Panel-dev/1Panel/agent/app/model" + "github.com/1Panel-dev/1Panel/agent/global" +) + +type ComposeTemplateRepo struct{} + +type IComposeTemplateRepo interface { + Get(opts ...DBOption) (model.ComposeTemplate, error) + List(opts ...DBOption) ([]model.ComposeTemplate, error) + Page(limit, offset int, opts ...DBOption) (int64, []model.ComposeTemplate, error) + Create(compose *model.ComposeTemplate) error + Update(id uint, vars map[string]interface{}) error + Delete(opts ...DBOption) error + + GetRecord(opts ...DBOption) (model.Compose, error) + CreateRecord(compose *model.Compose) error + DeleteRecord(opts ...DBOption) error + ListRecord() ([]model.Compose, error) +} + +func NewIComposeTemplateRepo() IComposeTemplateRepo { + return &ComposeTemplateRepo{} +} + +func (u *ComposeTemplateRepo) Get(opts ...DBOption) (model.ComposeTemplate, error) { + var compose model.ComposeTemplate + db := global.DB + for _, opt := range opts { + db = opt(db) + } + err := db.First(&compose).Error + return compose, err +} + +func (u *ComposeTemplateRepo) Page(page, size int, opts ...DBOption) (int64, []model.ComposeTemplate, error) { + var users []model.ComposeTemplate + db := global.DB.Model(&model.ComposeTemplate{}) + for _, opt := range opts { + db = opt(db) + } + count := int64(0) + db = db.Count(&count) + err := db.Limit(size).Offset(size * (page - 1)).Find(&users).Error + return count, users, err +} + +func (u *ComposeTemplateRepo) List(opts ...DBOption) ([]model.ComposeTemplate, error) { + var composes []model.ComposeTemplate + db := global.DB.Model(&model.ComposeTemplate{}) + for _, opt := range opts { + db = opt(db) + } + err := db.Find(&composes).Error + return composes, err +} + +func (u *ComposeTemplateRepo) Create(compose *model.ComposeTemplate) error { + return global.DB.Create(compose).Error +} + +func (u *ComposeTemplateRepo) Update(id uint, vars map[string]interface{}) error { + return global.DB.Model(&model.ComposeTemplate{}).Where("id = ?", id).Updates(vars).Error +} + +func (u *ComposeTemplateRepo) Delete(opts ...DBOption) error { + db := global.DB + for _, opt := range opts { + db = opt(db) + } + return db.Delete(&model.ComposeTemplate{}).Error +} + +func (u *ComposeTemplateRepo) GetRecord(opts ...DBOption) (model.Compose, error) { + var compose model.Compose + db := global.DB + for _, opt := range opts { + db = opt(db) + } + err := db.First(&compose).Error + return compose, err +} + +func (u *ComposeTemplateRepo) ListRecord() ([]model.Compose, error) { + var composes []model.Compose + if err := global.DB.Find(&composes).Error; err != nil { + return nil, err + } + return composes, nil +} + +func (u *ComposeTemplateRepo) CreateRecord(compose *model.Compose) error { + return global.DB.Create(compose).Error +} + +func (u *ComposeTemplateRepo) DeleteRecord(opts ...DBOption) error { + db := global.DB + for _, opt := range opts { + db = opt(db) + } + return db.Delete(&model.Compose{}).Error +} diff --git a/agent/app/repo/cronjob.go b/agent/app/repo/cronjob.go new file mode 100644 index 000000000..e95dd7757 --- /dev/null +++ b/agent/app/repo/cronjob.go @@ -0,0 +1,192 @@ +package repo + +import ( + "time" + + "github.com/1Panel-dev/1Panel/agent/app/model" + "github.com/1Panel-dev/1Panel/agent/constant" + "github.com/1Panel-dev/1Panel/agent/global" + "gorm.io/gorm" +) + +type CronjobRepo struct{} + +type ICronjobRepo interface { + Get(opts ...DBOption) (model.Cronjob, error) + GetRecord(opts ...DBOption) (model.JobRecords, error) + RecordFirst(id uint) (model.JobRecords, error) + ListRecord(opts ...DBOption) ([]model.JobRecords, error) + List(opts ...DBOption) ([]model.Cronjob, error) + Page(limit, offset int, opts ...DBOption) (int64, []model.Cronjob, error) + Create(cronjob *model.Cronjob) error + WithByJobID(id int) DBOption + WithByDbName(name string) DBOption + WithByDefaultDownload(account string) DBOption + WithByRecordDropID(id int) DBOption + WithByRecordFile(file string) DBOption + Save(id uint, cronjob model.Cronjob) error + Update(id uint, vars map[string]interface{}) error + Delete(opts ...DBOption) error + DeleteRecord(opts ...DBOption) error + StartRecords(cronjobID uint, fromLocal bool, targetPath string) model.JobRecords + UpdateRecords(id uint, vars map[string]interface{}) error + EndRecords(record model.JobRecords, status, message, records string) + PageRecords(page, size int, opts ...DBOption) (int64, []model.JobRecords, error) +} + +func NewICronjobRepo() ICronjobRepo { + return &CronjobRepo{} +} + +func (u *CronjobRepo) Get(opts ...DBOption) (model.Cronjob, error) { + var cronjob model.Cronjob + db := global.DB + for _, opt := range opts { + db = opt(db) + } + err := db.First(&cronjob).Error + return cronjob, err +} + +func (u *CronjobRepo) GetRecord(opts ...DBOption) (model.JobRecords, error) { + var record model.JobRecords + db := global.DB + for _, opt := range opts { + db = opt(db) + } + err := db.First(&record).Error + return record, err +} + +func (u *CronjobRepo) List(opts ...DBOption) ([]model.Cronjob, error) { + var cronjobs []model.Cronjob + db := global.DB.Model(&model.Cronjob{}) + for _, opt := range opts { + db = opt(db) + } + err := db.Find(&cronjobs).Error + return cronjobs, err +} + +func (u *CronjobRepo) ListRecord(opts ...DBOption) ([]model.JobRecords, error) { + var cronjobs []model.JobRecords + db := global.DB.Model(&model.JobRecords{}) + for _, opt := range opts { + db = opt(db) + } + err := db.Find(&cronjobs).Error + return cronjobs, err +} + +func (u *CronjobRepo) Page(page, size int, opts ...DBOption) (int64, []model.Cronjob, error) { + var cronjobs []model.Cronjob + db := global.DB.Model(&model.Cronjob{}) + for _, opt := range opts { + db = opt(db) + } + count := int64(0) + db = db.Count(&count) + err := db.Limit(size).Offset(size * (page - 1)).Find(&cronjobs).Error + return count, cronjobs, err +} + +func (u *CronjobRepo) RecordFirst(id uint) (model.JobRecords, error) { + var record model.JobRecords + err := global.DB.Where("cronjob_id = ?", id).Order("created_at desc").First(&record).Error + return record, err +} + +func (u *CronjobRepo) PageRecords(page, size int, opts ...DBOption) (int64, []model.JobRecords, error) { + var cronjobs []model.JobRecords + db := global.DB.Model(&model.JobRecords{}) + for _, opt := range opts { + db = opt(db) + } + count := int64(0) + db = db.Count(&count) + err := db.Order("created_at desc").Limit(size).Offset(size * (page - 1)).Find(&cronjobs).Error + return count, cronjobs, err +} + +func (u *CronjobRepo) Create(cronjob *model.Cronjob) error { + return global.DB.Create(cronjob).Error +} + +func (c *CronjobRepo) WithByJobID(id int) DBOption { + return func(g *gorm.DB) *gorm.DB { + return g.Where("cronjob_id = ?", id) + } +} + +func (c *CronjobRepo) WithByDbName(name string) DBOption { + return func(g *gorm.DB) *gorm.DB { + return g.Where("db_name = ?", name) + } +} + +func (c *CronjobRepo) WithByDefaultDownload(account string) DBOption { + return func(g *gorm.DB) *gorm.DB { + return g.Where("default_download = ?", account) + } +} + +func (c *CronjobRepo) WithByRecordFile(file string) DBOption { + return func(g *gorm.DB) *gorm.DB { + return g.Where("records = ?", file) + } +} + +func (c *CronjobRepo) WithByRecordDropID(id int) DBOption { + return func(g *gorm.DB) *gorm.DB { + return g.Where("id < ?", id) + } +} + +func (u *CronjobRepo) StartRecords(cronjobID uint, fromLocal bool, targetPath string) model.JobRecords { + var record model.JobRecords + record.StartTime = time.Now() + record.CronjobID = cronjobID + record.FromLocal = fromLocal + record.Status = constant.StatusWaiting + if err := global.DB.Create(&record).Error; err != nil { + global.LOG.Errorf("create record status failed, err: %v", err) + } + return record +} +func (u *CronjobRepo) EndRecords(record model.JobRecords, status, message, records string) { + errMap := make(map[string]interface{}) + errMap["records"] = records + errMap["status"] = status + errMap["file"] = record.File + errMap["message"] = message + errMap["interval"] = time.Since(record.StartTime).Milliseconds() + if err := global.DB.Model(&model.JobRecords{}).Where("id = ?", record.ID).Updates(errMap).Error; err != nil { + global.LOG.Errorf("update record status failed, err: %v", err) + } +} + +func (u *CronjobRepo) Save(id uint, cronjob model.Cronjob) error { + return global.DB.Model(&model.Cronjob{}).Where("id = ?", id).Save(&cronjob).Error +} +func (u *CronjobRepo) Update(id uint, vars map[string]interface{}) error { + return global.DB.Model(&model.Cronjob{}).Where("id = ?", id).Updates(vars).Error +} + +func (u *CronjobRepo) UpdateRecords(id uint, vars map[string]interface{}) error { + return global.DB.Model(&model.JobRecords{}).Where("id = ?", id).Updates(vars).Error +} + +func (u *CronjobRepo) Delete(opts ...DBOption) error { + db := global.DB + for _, opt := range opts { + db = opt(db) + } + return db.Delete(&model.Cronjob{}).Error +} +func (u *CronjobRepo) DeleteRecord(opts ...DBOption) error { + db := global.DB + for _, opt := range opts { + db = opt(db) + } + return db.Delete(&model.JobRecords{}).Error +} diff --git a/agent/app/repo/database.go b/agent/app/repo/database.go new file mode 100644 index 000000000..7905c9c4c --- /dev/null +++ b/agent/app/repo/database.go @@ -0,0 +1,143 @@ +package repo + +import ( + "context" + "fmt" + "strings" + + "github.com/1Panel-dev/1Panel/agent/app/model" + "github.com/1Panel-dev/1Panel/agent/global" + "github.com/1Panel-dev/1Panel/agent/utils/encrypt" + "gorm.io/gorm" +) + +type DatabaseRepo struct{} + +type IDatabaseRepo interface { + GetList(opts ...DBOption) ([]model.Database, error) + Page(limit, offset int, opts ...DBOption) (int64, []model.Database, error) + Create(ctx context.Context, database *model.Database) error + Update(id uint, vars map[string]interface{}) error + Delete(ctx context.Context, opts ...DBOption) error + Get(opts ...DBOption) (model.Database, error) + WithByFrom(from string) DBOption + WithoutByFrom(from string) DBOption + WithAppInstallID(appInstallID uint) DBOption + WithTypeList(dbType string) DBOption +} + +func NewIDatabaseRepo() IDatabaseRepo { + return &DatabaseRepo{} +} + +func (d *DatabaseRepo) Get(opts ...DBOption) (model.Database, error) { + var database model.Database + db := global.DB + for _, opt := range opts { + db = opt(db) + } + if err := db.First(&database).Error; err != nil { + return database, err + } + pass, err := encrypt.StringDecrypt(database.Password) + if err != nil { + global.LOG.Errorf("decrypt database %s password failed, err: %v", database.Name, err) + } + database.Password = pass + return database, nil +} + +func (d *DatabaseRepo) Page(page, size int, opts ...DBOption) (int64, []model.Database, error) { + var databases []model.Database + db := global.DB.Model(&model.Database{}) + for _, opt := range opts { + db = opt(db) + } + count := int64(0) + db = db.Count(&count) + if err := db.Limit(size).Offset(size * (page - 1)).Find(&databases).Error; err != nil { + return count, databases, err + } + for i := 0; i < len(databases); i++ { + pass, err := encrypt.StringDecrypt(databases[i].Password) + if err != nil { + global.LOG.Errorf("decrypt database db %s password failed, err: %v", databases[i].Name, err) + } + databases[i].Password = pass + } + return count, databases, nil +} + +func (d *DatabaseRepo) GetList(opts ...DBOption) ([]model.Database, error) { + var databases []model.Database + db := global.DB.Model(&model.Database{}) + for _, opt := range opts { + db = opt(db) + } + if err := db.Find(&databases).Error; err != nil { + return databases, err + } + for i := 0; i < len(databases); i++ { + pass, err := encrypt.StringDecrypt(databases[i].Password) + if err != nil { + global.LOG.Errorf("decrypt database db %s password failed, err: %v", databases[i].Name, err) + } + databases[i].Password = pass + } + return databases, nil +} + +func (d *DatabaseRepo) WithByFrom(from string) DBOption { + return func(g *gorm.DB) *gorm.DB { + return g.Where("`from` = ?", from) + } +} + +func (d *DatabaseRepo) WithoutByFrom(from string) DBOption { + return func(g *gorm.DB) *gorm.DB { + return g.Where("`from` != ?", from) + } +} + +func (d *DatabaseRepo) WithTypeList(dbType string) DBOption { + return func(g *gorm.DB) *gorm.DB { + if !strings.Contains(dbType, ",") { + return g.Where("`type` = ?", dbType) + } + types := strings.Split(dbType, ",") + var ( + rules []string + values []interface{} + ) + for _, ty := range types { + if len(ty) != 0 { + rules = append(rules, "`type` = ?") + values = append(values, ty) + } + } + return g.Where(strings.Join(rules, " OR "), values...) + } +} + +func (d *DatabaseRepo) WithAppInstallID(appInstallID uint) DBOption { + return func(g *gorm.DB) *gorm.DB { + return g.Where("app_install_id = ?", appInstallID) + } +} + +func (d *DatabaseRepo) Create(ctx context.Context, database *model.Database) error { + pass, err := encrypt.StringEncrypt(database.Password) + if err != nil { + return fmt.Errorf("decrypt database db %s password failed, err: %v", database.Name, err) + } + database.Password = pass + return getTx(ctx).Create(database).Error +} + +func (d *DatabaseRepo) Update(id uint, vars map[string]interface{}) error { + return global.DB.Model(&model.Database{}).Where("id = ?", id).Updates(vars).Error +} + +func (d *DatabaseRepo) Delete(ctx context.Context, opts ...DBOption) error { + return getTx(ctx, opts...).Delete(&model.Database{}).Error +} diff --git a/agent/app/repo/database_mysql.go b/agent/app/repo/database_mysql.go new file mode 100644 index 000000000..4d741bccd --- /dev/null +++ b/agent/app/repo/database_mysql.go @@ -0,0 +1,123 @@ +package repo + +import ( + "context" + "fmt" + + "github.com/1Panel-dev/1Panel/agent/app/model" + "github.com/1Panel-dev/1Panel/agent/global" + "github.com/1Panel-dev/1Panel/agent/utils/encrypt" + "gorm.io/gorm" +) + +type MysqlRepo struct{} + +type IMysqlRepo interface { + Get(opts ...DBOption) (model.DatabaseMysql, error) + WithByMysqlName(mysqlName string) DBOption + WithByFrom(from string) DBOption + List(opts ...DBOption) ([]model.DatabaseMysql, error) + Page(limit, offset int, opts ...DBOption) (int64, []model.DatabaseMysql, error) + Create(ctx context.Context, mysql *model.DatabaseMysql) error + Delete(ctx context.Context, opts ...DBOption) error + Update(id uint, vars map[string]interface{}) error + DeleteLocal(ctx context.Context) error +} + +func NewIMysqlRepo() IMysqlRepo { + return &MysqlRepo{} +} + +func (u *MysqlRepo) Get(opts ...DBOption) (model.DatabaseMysql, error) { + var mysql model.DatabaseMysql + db := global.DB + for _, opt := range opts { + db = opt(db) + } + if err := db.First(&mysql).Error; err != nil { + return mysql, err + } + + pass, err := encrypt.StringDecrypt(mysql.Password) + if err != nil { + global.LOG.Errorf("decrypt database db %s password failed, err: %v", mysql.Name, err) + } + mysql.Password = pass + return mysql, err +} + +func (u *MysqlRepo) List(opts ...DBOption) ([]model.DatabaseMysql, error) { + var mysqls []model.DatabaseMysql + db := global.DB.Model(&model.DatabaseMysql{}) + for _, opt := range opts { + db = opt(db) + } + if err := db.Find(&mysqls).Error; err != nil { + return mysqls, err + } + for i := 0; i < len(mysqls); i++ { + pass, err := encrypt.StringDecrypt(mysqls[i].Password) + if err != nil { + global.LOG.Errorf("decrypt database db %s password failed, err: %v", mysqls[i].Name, err) + } + mysqls[i].Password = pass + } + return mysqls, nil +} + +func (u *MysqlRepo) Page(page, size int, opts ...DBOption) (int64, []model.DatabaseMysql, error) { + var mysqls []model.DatabaseMysql + db := global.DB.Model(&model.DatabaseMysql{}) + for _, opt := range opts { + db = opt(db) + } + count := int64(0) + db = db.Count(&count) + if err := db.Limit(size).Offset(size * (page - 1)).Find(&mysqls).Error; err != nil { + return count, mysqls, err + } + for i := 0; i < len(mysqls); i++ { + pass, err := encrypt.StringDecrypt(mysqls[i].Password) + if err != nil { + global.LOG.Errorf("decrypt database db %s password failed, err: %v", mysqls[i].Name, err) + } + mysqls[i].Password = pass + } + return count, mysqls, nil +} + +func (u *MysqlRepo) Create(ctx context.Context, mysql *model.DatabaseMysql) error { + pass, err := encrypt.StringEncrypt(mysql.Password) + if err != nil { + return fmt.Errorf("decrypt database db %s password failed, err: %v", mysql.Name, err) + } + mysql.Password = pass + return getTx(ctx).Create(mysql).Error +} + +func (u *MysqlRepo) Delete(ctx context.Context, opts ...DBOption) error { + return getTx(ctx, opts...).Delete(&model.DatabaseMysql{}).Error +} + +func (u *MysqlRepo) DeleteLocal(ctx context.Context) error { + return getTx(ctx).Where("`from` = ?", "local").Delete(&model.DatabaseMysql{}).Error +} + +func (u *MysqlRepo) Update(id uint, vars map[string]interface{}) error { + return global.DB.Model(&model.DatabaseMysql{}).Where("id = ?", id).Updates(vars).Error +} + +func (u *MysqlRepo) WithByMysqlName(mysqlName string) DBOption { + return func(g *gorm.DB) *gorm.DB { + return g.Where("mysql_name = ?", mysqlName) + } +} + +func (u *MysqlRepo) WithByFrom(from string) DBOption { + return func(g *gorm.DB) *gorm.DB { + if len(from) != 0 { + return g.Where("`from` = ?", from) + } + return g + } +} diff --git a/agent/app/repo/database_postgresql.go b/agent/app/repo/database_postgresql.go new file mode 100644 index 000000000..e1f3b1896 --- /dev/null +++ b/agent/app/repo/database_postgresql.go @@ -0,0 +1,123 @@ +package repo + +import ( + "context" + "fmt" + + "github.com/1Panel-dev/1Panel/agent/app/model" + "github.com/1Panel-dev/1Panel/agent/global" + "github.com/1Panel-dev/1Panel/agent/utils/encrypt" + "gorm.io/gorm" +) + +type PostgresqlRepo struct{} + +type IPostgresqlRepo interface { + Get(opts ...DBOption) (model.DatabasePostgresql, error) + WithByPostgresqlName(postgresqlName string) DBOption + WithByFrom(from string) DBOption + List(opts ...DBOption) ([]model.DatabasePostgresql, error) + Page(limit, offset int, opts ...DBOption) (int64, []model.DatabasePostgresql, error) + Create(ctx context.Context, postgresql *model.DatabasePostgresql) error + Delete(ctx context.Context, opts ...DBOption) error + Update(id uint, vars map[string]interface{}) error + DeleteLocal(ctx context.Context) error +} + +func NewIPostgresqlRepo() IPostgresqlRepo { + return &PostgresqlRepo{} +} + +func (u *PostgresqlRepo) Get(opts ...DBOption) (model.DatabasePostgresql, error) { + var postgresql model.DatabasePostgresql + db := global.DB + for _, opt := range opts { + db = opt(db) + } + if err := db.First(&postgresql).Error; err != nil { + return postgresql, err + } + + pass, err := encrypt.StringDecrypt(postgresql.Password) + if err != nil { + global.LOG.Errorf("decrypt database db %s password failed, err: %v", postgresql.Name, err) + } + postgresql.Password = pass + return postgresql, err +} + +func (u *PostgresqlRepo) List(opts ...DBOption) ([]model.DatabasePostgresql, error) { + var postgresqls []model.DatabasePostgresql + db := global.DB.Model(&model.DatabasePostgresql{}) + for _, opt := range opts { + db = opt(db) + } + if err := db.Find(&postgresqls).Error; err != nil { + return postgresqls, err + } + for i := 0; i < len(postgresqls); i++ { + pass, err := encrypt.StringDecrypt(postgresqls[i].Password) + if err != nil { + global.LOG.Errorf("decrypt database db %s password failed, err: %v", postgresqls[i].Name, err) + } + postgresqls[i].Password = pass + } + return postgresqls, nil +} + +func (u *PostgresqlRepo) Page(page, size int, opts ...DBOption) (int64, []model.DatabasePostgresql, error) { + var postgresqls []model.DatabasePostgresql + db := global.DB.Model(&model.DatabasePostgresql{}) + for _, opt := range opts { + db = opt(db) + } + count := int64(0) + db = db.Count(&count) + if err := db.Limit(size).Offset(size * (page - 1)).Find(&postgresqls).Error; err != nil { + return count, postgresqls, err + } + for i := 0; i < len(postgresqls); i++ { + pass, err := encrypt.StringDecrypt(postgresqls[i].Password) + if err != nil { + global.LOG.Errorf("decrypt database db %s password failed, err: %v", postgresqls[i].Name, err) + } + postgresqls[i].Password = pass + } + return count, postgresqls, nil +} + +func (u *PostgresqlRepo) Create(ctx context.Context, postgresql *model.DatabasePostgresql) error { + pass, err := encrypt.StringEncrypt(postgresql.Password) + if err != nil { + return fmt.Errorf("decrypt database db %s password failed, err: %v", postgresql.Name, err) + } + postgresql.Password = pass + return getTx(ctx).Create(postgresql).Error +} + +func (u *PostgresqlRepo) Delete(ctx context.Context, opts ...DBOption) error { + return getTx(ctx, opts...).Delete(&model.DatabasePostgresql{}).Error +} + +func (u *PostgresqlRepo) DeleteLocal(ctx context.Context) error { + return getTx(ctx).Where("`from` = ?", "local").Delete(&model.DatabasePostgresql{}).Error +} + +func (u *PostgresqlRepo) Update(id uint, vars map[string]interface{}) error { + return global.DB.Model(&model.DatabasePostgresql{}).Where("id = ?", id).Updates(vars).Error +} + +func (u *PostgresqlRepo) WithByPostgresqlName(postgresqlName string) DBOption { + return func(g *gorm.DB) *gorm.DB { + return g.Where("postgresql_name = ?", postgresqlName) + } +} + +func (u *PostgresqlRepo) WithByFrom(from string) DBOption { + return func(g *gorm.DB) *gorm.DB { + if len(from) != 0 { + return g.Where("`from` = ?", from) + } + return g + } +} diff --git a/agent/app/repo/favorite.go b/agent/app/repo/favorite.go new file mode 100644 index 000000000..9fd394fe4 --- /dev/null +++ b/agent/app/repo/favorite.go @@ -0,0 +1,66 @@ +package repo + +import ( + "github.com/1Panel-dev/1Panel/agent/app/model" + "github.com/1Panel-dev/1Panel/agent/global" + "gorm.io/gorm" +) + +type FavoriteRepo struct{} + +type IFavoriteRepo interface { + Page(page, size int, opts ...DBOption) (int64, []model.Favorite, error) + Create(group *model.Favorite) error + Delete(opts ...DBOption) error + GetFirst(opts ...DBOption) (model.Favorite, error) + All() ([]model.Favorite, error) + WithByPath(path string) DBOption +} + +func NewIFavoriteRepo() IFavoriteRepo { + return &FavoriteRepo{} +} + +func (f *FavoriteRepo) WithByPath(path string) DBOption { + return func(db *gorm.DB) *gorm.DB { + return db.Where("path = ?", path) + } +} + +func (f *FavoriteRepo) Page(page, size int, opts ...DBOption) (int64, []model.Favorite, error) { + var ( + favorites []model.Favorite + count int64 + ) + count = int64(0) + db := getDb(opts...).Model(&model.Favorite{}) + db = db.Count(&count) + err := db.Limit(size).Offset(size * (page - 1)).Find(&favorites).Error + return count, favorites, err +} + +func (f *FavoriteRepo) Create(favorite *model.Favorite) error { + return global.DB.Create(favorite).Error +} + +func (f *FavoriteRepo) GetFirst(opts ...DBOption) (model.Favorite, error) { + var favorite model.Favorite + db := getDb(opts...).Model(&model.Favorite{}) + if err := db.First(&favorite).Error; err != nil { + return favorite, err + } + return favorite, nil +} + +func (f *FavoriteRepo) Delete(opts ...DBOption) error { + db := getDb(opts...).Model(&model.Favorite{}) + return db.Delete(&model.Favorite{}).Error +} + +func (f *FavoriteRepo) All() ([]model.Favorite, error) { + var favorites []model.Favorite + if err := getDb().Find(&favorites).Error; err != nil { + return nil, err + } + return favorites, nil +} diff --git a/agent/app/repo/ftp.go b/agent/app/repo/ftp.go new file mode 100644 index 000000000..8861b25ee --- /dev/null +++ b/agent/app/repo/ftp.go @@ -0,0 +1,80 @@ +package repo + +import ( + "github.com/1Panel-dev/1Panel/agent/app/model" + "github.com/1Panel-dev/1Panel/agent/global" + "gorm.io/gorm" +) + +type FtpRepo struct{} + +type IFtpRepo interface { + Get(opts ...DBOption) (model.Ftp, error) + GetList(opts ...DBOption) ([]model.Ftp, error) + Page(limit, offset int, opts ...DBOption) (int64, []model.Ftp, error) + Create(ftp *model.Ftp) error + Update(id uint, vars map[string]interface{}) error + Delete(opts ...DBOption) error + WithByUser(user string) DBOption +} + +func NewIFtpRepo() IFtpRepo { + return &FtpRepo{} +} + +func (u *FtpRepo) Get(opts ...DBOption) (model.Ftp, error) { + var ftp model.Ftp + db := global.DB + for _, opt := range opts { + db = opt(db) + } + err := db.First(&ftp).Error + return ftp, err +} + +func (h *FtpRepo) WithByUser(user string) DBOption { + return func(g *gorm.DB) *gorm.DB { + if len(user) == 0 { + return g + } + return g.Where("user like ?", "%"+user+"%") + } +} + +func (u *FtpRepo) GetList(opts ...DBOption) ([]model.Ftp, error) { + var ftps []model.Ftp + db := global.DB + for _, opt := range opts { + db = opt(db) + } + err := db.Find(&ftps).Error + return ftps, err +} + +func (h *FtpRepo) Page(page, size int, opts ...DBOption) (int64, []model.Ftp, error) { + var users []model.Ftp + db := global.DB.Model(&model.Ftp{}) + for _, opt := range opts { + db = opt(db) + } + count := int64(0) + db = db.Count(&count) + err := db.Limit(size).Offset(size * (page - 1)).Find(&users).Error + return count, users, err +} + +func (h *FtpRepo) Create(ftp *model.Ftp) error { + return global.DB.Create(ftp).Error +} + +func (h *FtpRepo) Update(id uint, vars map[string]interface{}) error { + return global.DB.Model(&model.Ftp{}).Where("id = ?", id).Updates(vars).Error +} + +func (h *FtpRepo) Delete(opts ...DBOption) error { + db := global.DB + for _, opt := range opts { + db = opt(db) + } + return db.Delete(&model.Ftp{}).Error +} diff --git a/agent/app/repo/group.go b/agent/app/repo/group.go new file mode 100644 index 000000000..1f5b27ae9 --- /dev/null +++ b/agent/app/repo/group.go @@ -0,0 +1,69 @@ +package repo + +import ( + "github.com/1Panel-dev/1Panel/agent/app/model" + "github.com/1Panel-dev/1Panel/agent/global" + "gorm.io/gorm" +) + +type GroupRepo struct{} + +type IGroupRepo interface { + Get(opts ...DBOption) (model.Group, error) + GetList(opts ...DBOption) ([]model.Group, error) + Create(group *model.Group) error + Update(id uint, vars map[string]interface{}) error + Delete(opts ...DBOption) error + CancelDefault(groupType string) error + WithByHostDefault() DBOption +} + +func NewIGroupRepo() IGroupRepo { + return &GroupRepo{} +} + +func (u *GroupRepo) Get(opts ...DBOption) (model.Group, error) { + var group model.Group + db := global.DB + for _, opt := range opts { + db = opt(db) + } + err := db.First(&group).Error + return group, err +} + +func (u *GroupRepo) GetList(opts ...DBOption) ([]model.Group, error) { + var groups []model.Group + db := global.DB.Model(&model.Group{}) + for _, opt := range opts { + db = opt(db) + } + err := db.Find(&groups).Error + return groups, err +} + +func (u *GroupRepo) Create(group *model.Group) error { + return global.DB.Create(group).Error +} + +func (u *GroupRepo) Update(id uint, vars map[string]interface{}) error { + return global.DB.Model(&model.Group{}).Where("id = ?", id).Updates(vars).Error +} + +func (u *GroupRepo) WithByHostDefault() DBOption { + return func(g *gorm.DB) *gorm.DB { + return g.Where("is_default = ? AND type = ?", 1, "host") + } +} + +func (u *GroupRepo) Delete(opts ...DBOption) error { + db := global.DB + for _, opt := range opts { + db = opt(db) + } + return db.Delete(&model.Group{}).Error +} + +func (u *GroupRepo) CancelDefault(groupType string) error { + return global.DB.Model(&model.Group{}).Where("is_default = ? AND type = ?", 1, groupType).Updates(map[string]interface{}{"is_default": 0}).Error +} diff --git a/agent/app/repo/host.go b/agent/app/repo/host.go new file mode 100644 index 000000000..eac052ab7 --- /dev/null +++ b/agent/app/repo/host.go @@ -0,0 +1,159 @@ +package repo + +import ( + "github.com/1Panel-dev/1Panel/agent/app/model" + "github.com/1Panel-dev/1Panel/agent/global" + "gorm.io/gorm" +) + +type HostRepo struct{} + +type IHostRepo interface { + Get(opts ...DBOption) (model.Host, error) + GetList(opts ...DBOption) ([]model.Host, error) + Page(limit, offset int, opts ...DBOption) (int64, []model.Host, error) + WithByInfo(info string) DBOption + WithByPort(port uint) DBOption + WithByUser(user string) DBOption + WithByAddr(addr string) DBOption + Create(host *model.Host) error + Update(id uint, vars map[string]interface{}) error + Delete(opts ...DBOption) error + + GetFirewallRecord(opts ...DBOption) (model.Firewall, error) + ListFirewallRecord() ([]model.Firewall, error) + SaveFirewallRecord(firewall *model.Firewall) error + DeleteFirewallRecordByID(id uint) error + DeleteFirewallRecord(fType, port, protocol, address, strategy string) error +} + +func NewIHostRepo() IHostRepo { + return &HostRepo{} +} + +func (h *HostRepo) Get(opts ...DBOption) (model.Host, error) { + var host model.Host + db := global.DB + for _, opt := range opts { + db = opt(db) + } + err := db.First(&host).Error + return host, err +} + +func (h *HostRepo) GetList(opts ...DBOption) ([]model.Host, error) { + var hosts []model.Host + db := global.DB.Model(&model.Host{}) + for _, opt := range opts { + db = opt(db) + } + err := db.Find(&hosts).Error + return hosts, err +} + +func (h *HostRepo) Page(page, size int, opts ...DBOption) (int64, []model.Host, error) { + var users []model.Host + db := global.DB.Model(&model.Host{}) + for _, opt := range opts { + db = opt(db) + } + count := int64(0) + db = db.Count(&count) + err := db.Limit(size).Offset(size * (page - 1)).Find(&users).Error + return count, users, err +} + +func (h *HostRepo) WithByInfo(info string) DBOption { + return func(g *gorm.DB) *gorm.DB { + if len(info) == 0 { + return g + } + infoStr := "%" + info + "%" + return g.Where("name LIKE ? OR addr LIKE ?", infoStr, infoStr) + } +} + +func (h *HostRepo) WithByPort(port uint) DBOption { + return func(g *gorm.DB) *gorm.DB { + return g.Where("port = ?", port) + } +} +func (h *HostRepo) WithByUser(user string) DBOption { + return func(g *gorm.DB) *gorm.DB { + return g.Where("user = ?", user) + } +} +func (h *HostRepo) WithByAddr(addr string) DBOption { + return func(g *gorm.DB) *gorm.DB { + return g.Where("addr = ?", addr) + } +} +func (h *HostRepo) WithByGroup(group string) DBOption { + return func(g *gorm.DB) *gorm.DB { + if len(group) == 0 { + return g + } + return g.Where("group_belong = ?", group) + } +} + +func (h *HostRepo) Create(host *model.Host) error { + return global.DB.Create(host).Error +} + +func (h *HostRepo) Update(id uint, vars map[string]interface{}) error { + return global.DB.Model(&model.Host{}).Where("id = ?", id).Updates(vars).Error +} + +func (h *HostRepo) Delete(opts ...DBOption) error { + db := global.DB + for _, opt := range opts { + db = opt(db) + } + return db.Delete(&model.Host{}).Error +} + +func (h *HostRepo) GetFirewallRecord(opts ...DBOption) (model.Firewall, error) { + var firewall model.Firewall + db := global.DB + for _, opt := range opts { + db = opt(db) + } + err := db.First(&firewall).Error + return firewall, err +} + +func (h *HostRepo) ListFirewallRecord() ([]model.Firewall, error) { + var datas []model.Firewall + if err := global.DB.Find(&datas).Error; err != nil { + return datas, nil + } + return datas, nil +} + +func (h *HostRepo) SaveFirewallRecord(firewall *model.Firewall) error { + if firewall.ID != 0 { + return global.DB.Save(firewall).Error + } + var data model.Firewall + if firewall.Type == "port" { + _ = global.DB.Where("type = ? AND port = ? AND protocol = ? AND address = ? AND strategy = ?", "port", firewall.Port, firewall.Protocol, firewall.Address, firewall.Strategy).First(&data) + if data.ID != 0 { + firewall.ID = data.ID + } + } else { + _ = global.DB.Where("type = ? AND address = ? AND strategy = ?", "address", firewall.Address, firewall.Strategy).First(&data) + if data.ID != 0 { + firewall.ID = data.ID + } + } + return global.DB.Save(firewall).Error +} + +func (h *HostRepo) DeleteFirewallRecordByID(id uint) error { + return global.DB.Where("id = ?", id).Delete(&model.Firewall{}).Error +} + +func (h *HostRepo) DeleteFirewallRecord(fType, port, protocol, address, strategy string) error { + return global.DB.Where("type = ? AND port = ? AND protocol = ? AND address = ? AND strategy = ?", fType, port, protocol, address, strategy).Delete(&model.Firewall{}).Error +} diff --git a/agent/app/repo/image_repo.go b/agent/app/repo/image_repo.go new file mode 100644 index 000000000..423fedff4 --- /dev/null +++ b/agent/app/repo/image_repo.go @@ -0,0 +1,71 @@ +package repo + +import ( + "github.com/1Panel-dev/1Panel/agent/app/model" + "github.com/1Panel-dev/1Panel/agent/global" +) + +type ImageRepoRepo struct{} + +type IImageRepoRepo interface { + Get(opts ...DBOption) (model.ImageRepo, error) + Page(limit, offset int, opts ...DBOption) (int64, []model.ImageRepo, error) + List(opts ...DBOption) ([]model.ImageRepo, error) + Create(imageRepo *model.ImageRepo) error + Update(id uint, vars map[string]interface{}) error + Delete(opts ...DBOption) error +} + +func NewIImageRepoRepo() IImageRepoRepo { + return &ImageRepoRepo{} +} + +func (u *ImageRepoRepo) Get(opts ...DBOption) (model.ImageRepo, error) { + var imageRepo model.ImageRepo + db := global.DB + for _, opt := range opts { + db = opt(db) + } + err := db.First(&imageRepo).Error + return imageRepo, err +} + +func (u *ImageRepoRepo) Page(page, size int, opts ...DBOption) (int64, []model.ImageRepo, error) { + var ops []model.ImageRepo + db := global.DB.Model(&model.ImageRepo{}) + for _, opt := range opts { + db = opt(db) + } + count := int64(0) + db = db.Count(&count) + err := db.Limit(size).Offset(size * (page - 1)).Find(&ops).Error + return count, ops, err +} + +func (u *ImageRepoRepo) List(opts ...DBOption) ([]model.ImageRepo, error) { + var ops []model.ImageRepo + db := global.DB.Model(&model.ImageRepo{}) + for _, opt := range opts { + db = opt(db) + } + count := int64(0) + db = db.Count(&count) + err := db.Find(&ops).Error + return ops, err +} + +func (u *ImageRepoRepo) Create(imageRepo *model.ImageRepo) error { + return global.DB.Create(imageRepo).Error +} + +func (u *ImageRepoRepo) Update(id uint, vars map[string]interface{}) error { + return global.DB.Model(&model.ImageRepo{}).Where("id = ?", id).Updates(vars).Error +} + +func (u *ImageRepoRepo) Delete(opts ...DBOption) error { + db := global.DB + for _, opt := range opts { + db = opt(db) + } + return db.Delete(&model.ImageRepo{}).Error +} diff --git a/agent/app/repo/php_extensions.go b/agent/app/repo/php_extensions.go new file mode 100644 index 000000000..651cf7d60 --- /dev/null +++ b/agent/app/repo/php_extensions.go @@ -0,0 +1,59 @@ +package repo + +import ( + "github.com/1Panel-dev/1Panel/agent/app/model" +) + +type PHPExtensionsRepo struct { +} + +type IPHPExtensionsRepo interface { + Page(page, size int, opts ...DBOption) (int64, []model.PHPExtensions, error) + Save(extension *model.PHPExtensions) error + Create(extension *model.PHPExtensions) error + GetFirst(opts ...DBOption) (model.PHPExtensions, error) + DeleteBy(opts ...DBOption) error + List() ([]model.PHPExtensions, error) +} + +func NewIPHPExtensionsRepo() IPHPExtensionsRepo { + return &PHPExtensionsRepo{} +} + +func (p *PHPExtensionsRepo) Page(page, size int, opts ...DBOption) (int64, []model.PHPExtensions, error) { + var ( + phpExtensions []model.PHPExtensions + ) + db := getDb(opts...).Model(&model.PHPExtensions{}) + count := int64(0) + db = db.Count(&count) + err := db.Limit(size).Offset(size * (page - 1)).Find(&phpExtensions).Error + return count, phpExtensions, err +} + +func (p *PHPExtensionsRepo) List() ([]model.PHPExtensions, error) { + var ( + phpExtensions []model.PHPExtensions + ) + err := getDb().Model(&model.PHPExtensions{}).Find(&phpExtensions).Error + return phpExtensions, err +} + +func (p *PHPExtensionsRepo) Save(extension *model.PHPExtensions) error { + return getDb().Save(&extension).Error +} + +func (p *PHPExtensionsRepo) Create(extension *model.PHPExtensions) error { + return getDb().Create(&extension).Error +} + +func (p *PHPExtensionsRepo) GetFirst(opts ...DBOption) (model.PHPExtensions, error) { + var extension model.PHPExtensions + db := getDb(opts...).Model(&model.PHPExtensions{}) + err := db.First(&extension).Error + return extension, err +} + +func (p *PHPExtensionsRepo) DeleteBy(opts ...DBOption) error { + return getDb(opts...).Delete(&model.PHPExtensions{}).Error +} diff --git a/agent/app/repo/runtime.go b/agent/app/repo/runtime.go new file mode 100644 index 000000000..b148d4585 --- /dev/null +++ b/agent/app/repo/runtime.go @@ -0,0 +1,107 @@ +package repo + +import ( + "context" + + "github.com/1Panel-dev/1Panel/agent/app/model" + "github.com/1Panel-dev/1Panel/agent/global" + "gorm.io/gorm" +) + +type RuntimeRepo struct { +} + +type IRuntimeRepo interface { + WithName(name string) DBOption + WithImage(image string) DBOption + WithNotId(id uint) DBOption + WithStatus(status string) DBOption + WithDetailId(id uint) DBOption + WithPort(port int) DBOption + Page(page, size int, opts ...DBOption) (int64, []model.Runtime, error) + Create(ctx context.Context, runtime *model.Runtime) error + Save(runtime *model.Runtime) error + DeleteBy(opts ...DBOption) error + GetFirst(opts ...DBOption) (*model.Runtime, error) + List(opts ...DBOption) ([]model.Runtime, error) +} + +func NewIRunTimeRepo() IRuntimeRepo { + return &RuntimeRepo{} +} + +func (r *RuntimeRepo) WithName(name string) DBOption { + return func(g *gorm.DB) *gorm.DB { + return g.Where("name = ?", name) + } +} + +func (r *RuntimeRepo) WithStatus(status string) DBOption { + return func(g *gorm.DB) *gorm.DB { + return g.Where("status = ?", status) + } +} + +func (r *RuntimeRepo) WithImage(image string) DBOption { + return func(g *gorm.DB) *gorm.DB { + return g.Where("image = ?", image) + } +} + +func (r *RuntimeRepo) WithDetailId(id uint) DBOption { + return func(g *gorm.DB) *gorm.DB { + return g.Where("app_detail_id = ?", id) + } +} + +func (r *RuntimeRepo) WithNotId(id uint) DBOption { + return func(g *gorm.DB) *gorm.DB { + return g.Where("id != ?", id) + } +} + +func (r *RuntimeRepo) WithPort(port int) DBOption { + return func(g *gorm.DB) *gorm.DB { + return g.Where("port = ?", port) + } +} + +func (r *RuntimeRepo) Page(page, size int, opts ...DBOption) (int64, []model.Runtime, error) { + var runtimes []model.Runtime + db := getDb(opts...).Model(&model.Runtime{}) + count := int64(0) + db = db.Count(&count) + err := db.Limit(size).Offset(size * (page - 1)).Find(&runtimes).Error + return count, runtimes, err +} + +func (r *RuntimeRepo) List(opts ...DBOption) ([]model.Runtime, error) { + var runtimes []model.Runtime + db := global.DB.Model(&model.Runtime{}) + for _, opt := range opts { + db = opt(db) + } + err := db.Find(&runtimes).Error + return runtimes, err +} + +func (r *RuntimeRepo) Create(ctx context.Context, runtime *model.Runtime) error { + db := getTx(ctx).Model(&model.Runtime{}) + return db.Create(&runtime).Error +} + +func (r *RuntimeRepo) Save(runtime *model.Runtime) error { + return getDb().Save(&runtime).Error +} + +func (r *RuntimeRepo) DeleteBy(opts ...DBOption) error { + return getDb(opts...).Delete(&model.Runtime{}).Error +} + +func (r *RuntimeRepo) GetFirst(opts ...DBOption) (*model.Runtime, error) { + var runtime model.Runtime + if err := getDb(opts...).First(&runtime).Error; err != nil { + return nil, err + } + return &runtime, nil +} diff --git a/agent/app/repo/setting.go b/agent/app/repo/setting.go new file mode 100644 index 000000000..55e4aa88d --- /dev/null +++ b/agent/app/repo/setting.go @@ -0,0 +1,87 @@ +package repo + +import ( + "time" + + "github.com/1Panel-dev/1Panel/agent/app/model" + "github.com/1Panel-dev/1Panel/agent/global" + "gorm.io/gorm" +) + +type SettingRepo struct{} + +type ISettingRepo interface { + GetList(opts ...DBOption) ([]model.Setting, error) + Get(opts ...DBOption) (model.Setting, error) + Create(key, value string) error + Update(key, value string) error + WithByKey(key string) DBOption + + CreateMonitorBase(model model.MonitorBase) error + BatchCreateMonitorIO(ioList []model.MonitorIO) error + BatchCreateMonitorNet(ioList []model.MonitorNetwork) error + DelMonitorBase(timeForDelete time.Time) error + DelMonitorIO(timeForDelete time.Time) error + DelMonitorNet(timeForDelete time.Time) error +} + +func NewISettingRepo() ISettingRepo { + return &SettingRepo{} +} + +func (u *SettingRepo) GetList(opts ...DBOption) ([]model.Setting, error) { + var settings []model.Setting + db := global.DB.Model(&model.Setting{}) + for _, opt := range opts { + db = opt(db) + } + err := db.Find(&settings).Error + return settings, err +} + +func (u *SettingRepo) Create(key, value string) error { + setting := &model.Setting{ + Key: key, + Value: value, + } + return global.DB.Create(setting).Error +} + +func (u *SettingRepo) Get(opts ...DBOption) (model.Setting, error) { + var settings model.Setting + db := global.DB.Model(&model.Setting{}) + for _, opt := range opts { + db = opt(db) + } + err := db.First(&settings).Error + return settings, err +} + +func (c *SettingRepo) WithByKey(key string) DBOption { + return func(g *gorm.DB) *gorm.DB { + return g.Where("key = ?", key) + } +} + +func (u *SettingRepo) Update(key, value string) error { + return global.DB.Model(&model.Setting{}).Where("key = ?", key).Updates(map[string]interface{}{"value": value}).Error +} + +func (u *SettingRepo) CreateMonitorBase(model model.MonitorBase) error { + return global.MonitorDB.Create(&model).Error +} +func (u *SettingRepo) BatchCreateMonitorIO(ioList []model.MonitorIO) error { + return global.MonitorDB.CreateInBatches(ioList, len(ioList)).Error +} +func (u *SettingRepo) BatchCreateMonitorNet(ioList []model.MonitorNetwork) error { + return global.MonitorDB.CreateInBatches(ioList, len(ioList)).Error +} +func (u *SettingRepo) DelMonitorBase(timeForDelete time.Time) error { + return global.MonitorDB.Where("created_at < ?", timeForDelete).Delete(&model.MonitorBase{}).Error +} +func (u *SettingRepo) DelMonitorIO(timeForDelete time.Time) error { + return global.MonitorDB.Where("created_at < ?", timeForDelete).Delete(&model.MonitorIO{}).Error +} +func (u *SettingRepo) DelMonitorNet(timeForDelete time.Time) error { + return global.MonitorDB.Where("created_at < ?", timeForDelete).Delete(&model.MonitorNetwork{}).Error +} diff --git a/agent/app/repo/snapshot.go b/agent/app/repo/snapshot.go new file mode 100644 index 000000000..dcb3dd606 --- /dev/null +++ b/agent/app/repo/snapshot.go @@ -0,0 +1,105 @@ +package repo + +import ( + "github.com/1Panel-dev/1Panel/agent/app/model" + "github.com/1Panel-dev/1Panel/agent/global" +) + +type ISnapshotRepo interface { + Get(opts ...DBOption) (model.Snapshot, error) + GetList(opts ...DBOption) ([]model.Snapshot, error) + Create(snap *model.Snapshot) error + Update(id uint, vars map[string]interface{}) error + Page(limit, offset int, opts ...DBOption) (int64, []model.Snapshot, error) + Delete(opts ...DBOption) error + + GetStatus(snapID uint) (model.SnapshotStatus, error) + GetStatusList(opts ...DBOption) ([]model.SnapshotStatus, error) + CreateStatus(snap *model.SnapshotStatus) error + DeleteStatus(snapID uint) error + UpdateStatus(id uint, vars map[string]interface{}) error +} + +func NewISnapshotRepo() ISnapshotRepo { + return &SnapshotRepo{} +} + +type SnapshotRepo struct{} + +func (u *SnapshotRepo) Get(opts ...DBOption) (model.Snapshot, error) { + var Snapshot model.Snapshot + db := global.DB + for _, opt := range opts { + db = opt(db) + } + err := db.First(&Snapshot).Error + return Snapshot, err +} + +func (u *SnapshotRepo) GetList(opts ...DBOption) ([]model.Snapshot, error) { + var snaps []model.Snapshot + db := global.DB.Model(&model.Snapshot{}) + for _, opt := range opts { + db = opt(db) + } + err := db.Find(&snaps).Error + return snaps, err +} + +func (u *SnapshotRepo) Page(page, size int, opts ...DBOption) (int64, []model.Snapshot, error) { + var users []model.Snapshot + db := global.DB.Model(&model.Snapshot{}) + for _, opt := range opts { + db = opt(db) + } + count := int64(0) + db = db.Count(&count) + err := db.Limit(size).Offset(size * (page - 1)).Find(&users).Error + return count, users, err +} + +func (u *SnapshotRepo) Create(Snapshot *model.Snapshot) error { + return global.DB.Create(Snapshot).Error +} + +func (u *SnapshotRepo) Update(id uint, vars map[string]interface{}) error { + return global.DB.Model(&model.Snapshot{}).Where("id = ?", id).Updates(vars).Error +} + +func (u *SnapshotRepo) Delete(opts ...DBOption) error { + db := global.DB + for _, opt := range opts { + db = opt(db) + } + return db.Delete(&model.Snapshot{}).Error +} + +func (u *SnapshotRepo) GetStatus(snapID uint) (model.SnapshotStatus, error) { + var data model.SnapshotStatus + if err := global.DB.Where("snap_id = ?", snapID).First(&data).Error; err != nil { + return data, err + } + return data, nil +} + +func (u *SnapshotRepo) GetStatusList(opts ...DBOption) ([]model.SnapshotStatus, error) { + var status []model.SnapshotStatus + db := global.DB.Model(&model.SnapshotStatus{}) + for _, opt := range opts { + db = opt(db) + } + err := db.Find(&status).Error + return status, err +} + +func (u *SnapshotRepo) CreateStatus(snap *model.SnapshotStatus) error { + return global.DB.Create(snap).Error +} + +func (u *SnapshotRepo) DeleteStatus(snapID uint) error { + return global.DB.Where("snap_id = ?", snapID).Delete(&model.SnapshotStatus{}).Error +} + +func (u *SnapshotRepo) UpdateStatus(id uint, vars map[string]interface{}) error { + return global.DB.Model(&model.SnapshotStatus{}).Where("id = ?", id).Updates(vars).Error +} diff --git a/agent/app/repo/tag.go b/agent/app/repo/tag.go new file mode 100644 index 000000000..9f008bcd2 --- /dev/null +++ b/agent/app/repo/tag.go @@ -0,0 +1,63 @@ +package repo + +import ( + "context" + + "github.com/1Panel-dev/1Panel/agent/app/model" +) + +type TagRepo struct { +} + +type ITagRepo interface { + BatchCreate(ctx context.Context, tags []*model.Tag) error + DeleteAll(ctx context.Context) error + All() ([]model.Tag, error) + GetByIds(ids []uint) ([]model.Tag, error) + GetByKeys(keys []string) ([]model.Tag, error) + GetByAppId(appId uint) ([]model.Tag, error) +} + +func NewITagRepo() ITagRepo { + return &TagRepo{} +} + +func (t TagRepo) BatchCreate(ctx context.Context, tags []*model.Tag) error { + return getTx(ctx).Create(&tags).Error +} + +func (t TagRepo) DeleteAll(ctx context.Context) error { + return getTx(ctx).Where("1 = 1 ").Delete(&model.Tag{}).Error +} + +func (t TagRepo) All() ([]model.Tag, error) { + var tags []model.Tag + if err := getDb().Where("1 = 1 ").Order("sort asc").Find(&tags).Error; err != nil { + return nil, err + } + return tags, nil +} + +func (t TagRepo) GetByIds(ids []uint) ([]model.Tag, error) { + var tags []model.Tag + if err := getDb().Where("id in (?)", ids).Find(&tags).Error; err != nil { + return nil, err + } + return tags, nil +} + +func (t TagRepo) GetByKeys(keys []string) ([]model.Tag, error) { + var tags []model.Tag + if err := getDb().Where("key in (?)", keys).Find(&tags).Error; err != nil { + return nil, err + } + return tags, nil +} + +func (t TagRepo) GetByAppId(appId uint) ([]model.Tag, error) { + var tags []model.Tag + if err := getDb().Where("id in (select tag_id from app_tags where app_id = ?)", appId).Find(&tags).Error; err != nil { + return nil, err + } + return tags, nil +} diff --git a/agent/app/repo/website.go b/agent/app/repo/website.go new file mode 100644 index 000000000..4cb535b8a --- /dev/null +++ b/agent/app/repo/website.go @@ -0,0 +1,145 @@ +package repo + +import ( + "context" + + "github.com/1Panel-dev/1Panel/agent/app/model" + "github.com/1Panel-dev/1Panel/agent/global" + "gorm.io/gorm" + "gorm.io/gorm/clause" +) + +type IWebsiteRepo interface { + WithAppInstallId(appInstallId uint) DBOption + WithDomain(domain string) DBOption + WithAlias(alias string) DBOption + WithWebsiteSSLID(sslId uint) DBOption + WithGroupID(groupId uint) DBOption + 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) + GetBy(opts ...DBOption) ([]model.Website, error) + Save(ctx context.Context, app *model.Website) error + SaveWithoutCtx(app *model.Website) error + DeleteBy(ctx context.Context, opts ...DBOption) error + Create(ctx context.Context, app *model.Website) error + DeleteAll(ctx context.Context) error +} + +func NewIWebsiteRepo() IWebsiteRepo { + return &WebsiteRepo{} +} + +type WebsiteRepo struct { +} + +func (w *WebsiteRepo) WithAppInstallId(appInstallID uint) DBOption { + return func(db *gorm.DB) *gorm.DB { + 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) + } +} + +func (w *WebsiteRepo) WithRuntimeID(runtimeID uint) DBOption { + return func(db *gorm.DB) *gorm.DB { + return db.Where("runtime_id = ?", runtimeID) + } +} + +func (w *WebsiteRepo) WithDomain(domain string) DBOption { + return func(db *gorm.DB) *gorm.DB { + return db.Where("primary_domain = ?", domain) + } +} + +func (w *WebsiteRepo) WithDomainLike(domain string) DBOption { + return func(db *gorm.DB) *gorm.DB { + return db.Where("primary_domain like ?", "%"+domain+"%") + } +} + +func (w *WebsiteRepo) WithAlias(alias string) DBOption { + return func(db *gorm.DB) *gorm.DB { + return db.Where("alias = ?", alias) + } +} + +func (w *WebsiteRepo) WithWebsiteSSLID(sslId uint) DBOption { + return func(db *gorm.DB) *gorm.DB { + return db.Where("website_ssl_id = ?", sslId) + } +} + +func (w *WebsiteRepo) WithGroupID(groupId uint) DBOption { + return func(db *gorm.DB) *gorm.DB { + return db.Where("website_group_id = ?", groupId) + } +} + +func (w *WebsiteRepo) WithDefaultServer() DBOption { + return func(db *gorm.DB) *gorm.DB { + return db.Where("default_server = 1") + } +} + +func (w *WebsiteRepo) Page(page, size int, opts ...DBOption) (int64, []model.Website, error) { + var websites []model.Website + db := getDb(opts...).Model(&model.Website{}) + count := int64(0) + db = db.Count(&count) + err := db.Debug().Limit(size).Offset(size * (page - 1)).Preload("WebsiteSSL").Find(&websites).Error + return count, websites, err +} + +func (w *WebsiteRepo) List(opts ...DBOption) ([]model.Website, error) { + var websites []model.Website + err := getDb(opts...).Model(&model.Website{}).Preload("Domains").Preload("WebsiteSSL").Find(&websites).Error + return websites, err +} + +func (w *WebsiteRepo) GetFirst(opts ...DBOption) (model.Website, error) { + var website model.Website + db := getDb(opts...).Model(&model.Website{}) + if err := db.Preload("Domains").First(&website).Error; err != nil { + return website, err + } + return website, nil +} + +func (w *WebsiteRepo) GetBy(opts ...DBOption) ([]model.Website, error) { + var websites []model.Website + db := getDb(opts...).Model(&model.Website{}) + if err := db.Find(&websites).Error; err != nil { + return websites, err + } + return websites, nil +} + +func (w *WebsiteRepo) Create(ctx context.Context, app *model.Website) error { + return getTx(ctx).Omit(clause.Associations).Create(app).Error +} + +func (w *WebsiteRepo) Save(ctx context.Context, app *model.Website) error { + return getTx(ctx).Omit(clause.Associations).Save(app).Error +} + +func (w *WebsiteRepo) SaveWithoutCtx(website *model.Website) error { + return global.DB.Save(website).Error +} + +func (w *WebsiteRepo) DeleteBy(ctx context.Context, opts ...DBOption) error { + return getTx(ctx, opts...).Delete(&model.Website{}).Error +} + +func (w *WebsiteRepo) DeleteAll(ctx context.Context) error { + return getTx(ctx).Where("1 = 1 ").Delete(&model.Website{}).Error +} diff --git a/agent/app/repo/website_acme_account.go b/agent/app/repo/website_acme_account.go new file mode 100644 index 000000000..f2708e866 --- /dev/null +++ b/agent/app/repo/website_acme_account.go @@ -0,0 +1,64 @@ +package repo + +import ( + "github.com/1Panel-dev/1Panel/agent/app/model" + "gorm.io/gorm" +) + +type IAcmeAccountRepo interface { + Page(page, size int, opts ...DBOption) (int64, []model.WebsiteAcmeAccount, error) + GetFirst(opts ...DBOption) (*model.WebsiteAcmeAccount, error) + Create(account model.WebsiteAcmeAccount) error + Save(account model.WebsiteAcmeAccount) error + DeleteBy(opts ...DBOption) error + WithEmail(email string) DBOption + WithType(acType string) DBOption +} + +func NewIAcmeAccountRepo() IAcmeAccountRepo { + return &WebsiteAcmeAccountRepo{} +} + +type WebsiteAcmeAccountRepo struct { +} + +func (w *WebsiteAcmeAccountRepo) WithEmail(email string) DBOption { + return func(db *gorm.DB) *gorm.DB { + return db.Where("email = ?", email) + } +} +func (w *WebsiteAcmeAccountRepo) WithType(acType string) DBOption { + return func(db *gorm.DB) *gorm.DB { + return db.Where("type = ?", acType) + } +} + +func (w *WebsiteAcmeAccountRepo) Page(page, size int, opts ...DBOption) (int64, []model.WebsiteAcmeAccount, error) { + var accounts []model.WebsiteAcmeAccount + db := getDb(opts...).Model(&model.WebsiteAcmeAccount{}) + count := int64(0) + db = db.Count(&count) + err := db.Limit(size).Offset(size * (page - 1)).Find(&accounts).Error + return count, accounts, err +} + +func (w *WebsiteAcmeAccountRepo) GetFirst(opts ...DBOption) (*model.WebsiteAcmeAccount, error) { + var account model.WebsiteAcmeAccount + db := getDb(opts...).Model(&model.WebsiteAcmeAccount{}) + if err := db.First(&account).Error; err != nil { + return nil, err + } + return &account, nil +} + +func (w *WebsiteAcmeAccountRepo) Create(account model.WebsiteAcmeAccount) error { + return getDb().Create(&account).Error +} + +func (w *WebsiteAcmeAccountRepo) Save(account model.WebsiteAcmeAccount) error { + return getDb().Save(&account).Error +} + +func (w *WebsiteAcmeAccountRepo) DeleteBy(opts ...DBOption) error { + return getDb(opts...).Debug().Delete(&model.WebsiteAcmeAccount{}).Error +} diff --git a/agent/app/repo/website_ca.go b/agent/app/repo/website_ca.go new file mode 100644 index 000000000..dd8a8f3cd --- /dev/null +++ b/agent/app/repo/website_ca.go @@ -0,0 +1,55 @@ +package repo + +import ( + "context" + + "github.com/1Panel-dev/1Panel/agent/app/model" +) + +type WebsiteCARepo struct { +} + +func NewIWebsiteCARepo() IWebsiteCARepo { + return &WebsiteCARepo{} +} + +type IWebsiteCARepo interface { + Page(page, size int, opts ...DBOption) (int64, []model.WebsiteCA, error) + GetFirst(opts ...DBOption) (model.WebsiteCA, error) + List(opts ...DBOption) ([]model.WebsiteCA, error) + Create(ctx context.Context, ca *model.WebsiteCA) error + DeleteBy(opts ...DBOption) error +} + +func (w WebsiteCARepo) Page(page, size int, opts ...DBOption) (int64, []model.WebsiteCA, error) { + var caList []model.WebsiteCA + db := getDb(opts...).Model(&model.WebsiteCA{}) + count := int64(0) + db = db.Count(&count) + err := db.Limit(size).Offset(size * (page - 1)).Find(&caList).Error + return count, caList, err +} + +func (w WebsiteCARepo) GetFirst(opts ...DBOption) (model.WebsiteCA, error) { + var ca model.WebsiteCA + db := getDb(opts...).Model(&model.WebsiteCA{}) + if err := db.First(&ca).Error; err != nil { + return ca, err + } + return ca, nil +} + +func (w WebsiteCARepo) List(opts ...DBOption) ([]model.WebsiteCA, error) { + var caList []model.WebsiteCA + db := getDb(opts...).Model(&model.WebsiteCA{}) + err := db.Find(&caList).Error + return caList, err +} + +func (w WebsiteCARepo) Create(ctx context.Context, ca *model.WebsiteCA) error { + return getTx(ctx).Create(ca).Error +} + +func (w WebsiteCARepo) DeleteBy(opts ...DBOption) error { + return getDb(opts...).Delete(&model.WebsiteCA{}).Error +} diff --git a/agent/app/repo/website_dns_account.go b/agent/app/repo/website_dns_account.go new file mode 100644 index 000000000..b94590364 --- /dev/null +++ b/agent/app/repo/website_dns_account.go @@ -0,0 +1,60 @@ +package repo + +import ( + "github.com/1Panel-dev/1Panel/agent/app/model" +) + +type WebsiteDnsAccountRepo struct { +} + +type IWebsiteDnsAccountRepo interface { + Page(page, size int, opts ...DBOption) (int64, []model.WebsiteDnsAccount, error) + GetFirst(opts ...DBOption) (*model.WebsiteDnsAccount, error) + List(opts ...DBOption) ([]model.WebsiteDnsAccount, error) + Create(account model.WebsiteDnsAccount) error + Save(account model.WebsiteDnsAccount) error + DeleteBy(opts ...DBOption) error +} + +func NewIWebsiteDnsAccountRepo() IWebsiteDnsAccountRepo { + return &WebsiteDnsAccountRepo{} +} + +func (w WebsiteDnsAccountRepo) Page(page, size int, opts ...DBOption) (int64, []model.WebsiteDnsAccount, error) { + var accounts []model.WebsiteDnsAccount + db := getDb(opts...).Model(&model.WebsiteDnsAccount{}) + count := int64(0) + db = db.Count(&count) + err := db.Limit(size).Offset(size * (page - 1)).Find(&accounts).Error + return count, accounts, err +} + +func (w WebsiteDnsAccountRepo) GetFirst(opts ...DBOption) (*model.WebsiteDnsAccount, error) { + var account model.WebsiteDnsAccount + db := getDb(opts...).Model(&model.WebsiteDnsAccount{}) + if err := db.First(&account).Error; err != nil { + return nil, err + } + return &account, nil +} + +func (w WebsiteDnsAccountRepo) List(opts ...DBOption) ([]model.WebsiteDnsAccount, error) { + var accounts []model.WebsiteDnsAccount + db := getDb(opts...).Model(&model.WebsiteDnsAccount{}) + if err := db.Find(&accounts).Error; err != nil { + return nil, err + } + return accounts, nil +} + +func (w WebsiteDnsAccountRepo) Create(account model.WebsiteDnsAccount) error { + return getDb().Create(&account).Error +} + +func (w WebsiteDnsAccountRepo) Save(account model.WebsiteDnsAccount) error { + return getDb().Save(&account).Error +} + +func (w WebsiteDnsAccountRepo) DeleteBy(opts ...DBOption) error { + return getDb(opts...).Delete(&model.WebsiteDnsAccount{}).Error +} diff --git a/agent/app/repo/website_domain.go b/agent/app/repo/website_domain.go new file mode 100644 index 000000000..40258aace --- /dev/null +++ b/agent/app/repo/website_domain.go @@ -0,0 +1,99 @@ +package repo + +import ( + "context" + + "github.com/1Panel-dev/1Panel/agent/app/model" + "gorm.io/gorm" + "gorm.io/gorm/clause" +) + +type WebsiteDomainRepo struct { +} + +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) + BatchCreate(ctx context.Context, domains []model.WebsiteDomain) error + Create(ctx context.Context, app *model.WebsiteDomain) error + Save(ctx context.Context, app *model.WebsiteDomain) error + DeleteBy(ctx context.Context, opts ...DBOption) error + DeleteAll(ctx context.Context) error +} + +func NewIWebsiteDomainRepo() IWebsiteDomainRepo { + return &WebsiteDomainRepo{} +} + +func (w WebsiteDomainRepo) WithWebsiteId(websiteId uint) DBOption { + return func(db *gorm.DB) *gorm.DB { + return db.Where("website_id = ?", websiteId) + } +} + +func (w WebsiteDomainRepo) WithPort(port int) DBOption { + return func(db *gorm.DB) *gorm.DB { + return db.Where("port = ?", port) + } +} +func (w WebsiteDomainRepo) WithDomain(domain string) DBOption { + return func(db *gorm.DB) *gorm.DB { + 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{}) + count := int64(0) + db = db.Count(&count) + err := db.Limit(size).Offset(size * (page - 1)).Find(&domains).Error + return count, domains, err +} + +func (w WebsiteDomainRepo) GetFirst(opts ...DBOption) (model.WebsiteDomain, error) { + var domain model.WebsiteDomain + db := getDb(opts...).Model(&model.WebsiteDomain{}) + if err := db.First(&domain).Error; err != nil { + return domain, err + } + return domain, nil +} + +func (w WebsiteDomainRepo) GetBy(opts ...DBOption) ([]model.WebsiteDomain, error) { + var domains []model.WebsiteDomain + db := getDb(opts...).Model(&model.WebsiteDomain{}) + if err := db.Find(&domains).Error; err != nil { + return domains, err + } + return domains, nil +} + +func (w WebsiteDomainRepo) BatchCreate(ctx context.Context, domains []model.WebsiteDomain) error { + return getTx(ctx).Model(&model.WebsiteDomain{}).Create(&domains).Error +} + +func (w WebsiteDomainRepo) Create(ctx context.Context, app *model.WebsiteDomain) error { + return getTx(ctx).Omit(clause.Associations).Create(app).Error +} + +func (w WebsiteDomainRepo) Save(ctx context.Context, app *model.WebsiteDomain) error { + return getTx(ctx).Omit(clause.Associations).Save(app).Error +} + +func (w WebsiteDomainRepo) DeleteBy(ctx context.Context, opts ...DBOption) error { + return getTx(ctx, opts...).Delete(&model.WebsiteDomain{}).Error +} + +func (w WebsiteDomainRepo) DeleteAll(ctx context.Context) error { + return getTx(ctx).Where("1 = 1 ").Delete(&model.WebsiteDomain{}).Error +} diff --git a/agent/app/repo/website_ssl.go b/agent/app/repo/website_ssl.go new file mode 100644 index 000000000..8d98fef58 --- /dev/null +++ b/agent/app/repo/website_ssl.go @@ -0,0 +1,100 @@ +package repo + +import ( + "context" + + "github.com/1Panel-dev/1Panel/agent/app/model" + "gorm.io/gorm" +) + +func NewISSLRepo() ISSLRepo { + return &WebsiteSSLRepo{} +} + +type ISSLRepo interface { + WithByAlias(alias string) DBOption + WithByAcmeAccountId(acmeAccountId uint) DBOption + WithByDnsAccountId(dnsAccountId uint) DBOption + WithByCAID(caID uint) DBOption + Page(page, size int, opts ...DBOption) (int64, []model.WebsiteSSL, error) + GetFirst(opts ...DBOption) (*model.WebsiteSSL, error) + List(opts ...DBOption) ([]model.WebsiteSSL, error) + Create(ctx context.Context, ssl *model.WebsiteSSL) error + Save(ssl *model.WebsiteSSL) error + DeleteBy(opts ...DBOption) error + SaveByMap(ssl *model.WebsiteSSL, params map[string]interface{}) error +} + +type WebsiteSSLRepo struct { +} + +func (w WebsiteSSLRepo) WithByAlias(alias string) DBOption { + return func(db *gorm.DB) *gorm.DB { + return db.Where("alias = ?", alias) + } +} + +func (w WebsiteSSLRepo) WithByAcmeAccountId(acmeAccountId uint) DBOption { + return func(db *gorm.DB) *gorm.DB { + return db.Where("acme_account_id = ?", acmeAccountId) + } +} + +func (w WebsiteSSLRepo) WithByDnsAccountId(dnsAccountId uint) DBOption { + return func(db *gorm.DB) *gorm.DB { + return db.Where("dns_account_id = ?", dnsAccountId) + } +} + +func (w WebsiteSSLRepo) WithByCAID(caID uint) DBOption { + return func(db *gorm.DB) *gorm.DB { + return db.Where("ca_id = ?", caID) + } +} + +func (w WebsiteSSLRepo) Page(page, size int, opts ...DBOption) (int64, []model.WebsiteSSL, error) { + var sslList []model.WebsiteSSL + db := getDb(opts...).Model(&model.WebsiteSSL{}) + count := int64(0) + db = db.Count(&count) + err := db.Limit(size).Offset(size * (page - 1)).Preload("AcmeAccount").Preload("DnsAccount").Preload("Websites").Find(&sslList).Error + return count, sslList, err +} + +func (w WebsiteSSLRepo) GetFirst(opts ...DBOption) (*model.WebsiteSSL, error) { + var website *model.WebsiteSSL + db := getDb(opts...).Model(&model.WebsiteSSL{}) + if err := db.Preload("AcmeAccount").Preload("DnsAccount").First(&website).Error; err != nil { + return website, err + } + return website, nil +} + +func (w WebsiteSSLRepo) List(opts ...DBOption) ([]model.WebsiteSSL, error) { + var websites []model.WebsiteSSL + db := getDb(opts...).Model(&model.WebsiteSSL{}) + if err := db.Preload("AcmeAccount").Preload("DnsAccount").Find(&websites).Error; err != nil { + return websites, err + } + return websites, nil +} + +func (w WebsiteSSLRepo) Create(ctx context.Context, ssl *model.WebsiteSSL) error { + return getTx(ctx).Create(ssl).Error +} + +func (w WebsiteSSLRepo) Save(ssl *model.WebsiteSSL) error { + return getDb().Model(&model.WebsiteSSL{BaseModel: model.BaseModel{ + ID: ssl.ID, + }}).Save(&ssl).Error +} + +func (w WebsiteSSLRepo) SaveByMap(ssl *model.WebsiteSSL, params map[string]interface{}) error { + return getDb().Model(&model.WebsiteSSL{BaseModel: model.BaseModel{ + ID: ssl.ID, + }}).Updates(params).Error +} + +func (w WebsiteSSLRepo) DeleteBy(opts ...DBOption) error { + return getDb(opts...).Delete(&model.WebsiteSSL{}).Error +} diff --git a/agent/app/service/app.go b/agent/app/service/app.go new file mode 100644 index 000000000..2d58e6ddd --- /dev/null +++ b/agent/app/service/app.go @@ -0,0 +1,1028 @@ +package service + +import ( + "context" + "encoding/base64" + "encoding/json" + "fmt" + "net/http" + "os" + "path/filepath" + "reflect" + "strconv" + "strings" + + "github.com/1Panel-dev/1Panel/agent/app/dto" + "github.com/1Panel-dev/1Panel/agent/app/dto/request" + "github.com/1Panel-dev/1Panel/agent/app/dto/response" + "github.com/1Panel-dev/1Panel/agent/app/model" + "github.com/1Panel-dev/1Panel/agent/app/repo" + "github.com/1Panel-dev/1Panel/agent/buserr" + "github.com/1Panel-dev/1Panel/agent/constant" + "github.com/1Panel-dev/1Panel/agent/global" + "github.com/1Panel-dev/1Panel/agent/i18n" + "github.com/1Panel-dev/1Panel/agent/utils/common" + "github.com/1Panel-dev/1Panel/agent/utils/docker" + "github.com/1Panel-dev/1Panel/agent/utils/files" + http2 "github.com/1Panel-dev/1Panel/agent/utils/http" + httpUtil "github.com/1Panel-dev/1Panel/agent/utils/http" + "github.com/1Panel-dev/1Panel/agent/utils/xpack" + "gopkg.in/yaml.v3" +) + +type AppService struct { +} + +type IAppService interface { + PageApp(req request.AppSearch) (interface{}, error) + GetAppTags() ([]response.TagDTO, error) + GetApp(key string) (*response.AppDTO, error) + GetAppDetail(appId uint, version, appType string) (response.AppDetailDTO, error) + Install(ctx context.Context, req request.AppInstallCreate) (*model.AppInstall, error) + SyncAppListFromRemote() error + GetAppUpdate() (*response.AppUpdateRes, error) + GetAppDetailByID(id uint) (*response.AppDetailDTO, error) + SyncAppListFromLocal() + GetIgnoredApp() ([]response.IgnoredApp, error) +} + +func NewIAppService() IAppService { + return &AppService{} +} + +func (a AppService) PageApp(req request.AppSearch) (interface{}, error) { + var opts []repo.DBOption + opts = append(opts, appRepo.OrderByRecommend()) + if req.Name != "" { + opts = append(opts, appRepo.WithLikeName(req.Name)) + } + if req.Type != "" { + opts = append(opts, appRepo.WithType(req.Type)) + } + if req.Recommend { + opts = append(opts, appRepo.GetRecommend()) + } + if req.Resource != "" && req.Resource != "all" { + opts = append(opts, appRepo.WithResource(req.Resource)) + } + if len(req.Tags) != 0 { + tags, err := tagRepo.GetByKeys(req.Tags) + if err != nil { + return nil, err + } + var tagIds []uint + for _, t := range tags { + tagIds = append(tagIds, t.ID) + } + appTags, err := appTagRepo.GetByTagIds(tagIds) + if err != nil { + return nil, err + } + var appIds []uint + for _, t := range appTags { + appIds = append(appIds, t.AppId) + } + opts = append(opts, commonRepo.WithIdsIn(appIds)) + } + var res response.AppRes + total, apps, err := appRepo.Page(req.Page, req.PageSize, opts...) + if err != nil { + return nil, err + } + var appDTOs []*response.AppDto + for _, ap := range apps { + ap.ReadMe = "" + ap.Website = "" + ap.Document = "" + ap.Github = "" + appDTO := &response.AppDto{ + ID: ap.ID, + Name: ap.Name, + Key: ap.Key, + Type: ap.Type, + Icon: ap.Icon, + ShortDescZh: ap.ShortDescZh, + ShortDescEn: ap.ShortDescEn, + Resource: ap.Resource, + Limit: ap.Limit, + } + appDTOs = append(appDTOs, appDTO) + appTags, err := appTagRepo.GetByAppId(ap.ID) + if err != nil { + continue + } + var tagIds []uint + for _, at := range appTags { + tagIds = append(tagIds, at.TagId) + } + tags, err := tagRepo.GetByIds(tagIds) + if err != nil { + continue + } + appDTO.Tags = tags + installs, _ := appInstallRepo.ListBy(appInstallRepo.WithAppId(ap.ID)) + appDTO.Installed = len(installs) > 0 + } + res.Items = appDTOs + res.Total = total + + return res, nil +} + +func (a AppService) GetAppTags() ([]response.TagDTO, error) { + tags, err := tagRepo.All() + if err != nil { + return nil, err + } + var res []response.TagDTO + for _, tag := range tags { + res = append(res, response.TagDTO{ + Tag: tag, + }) + } + return res, nil +} + +func (a AppService) GetApp(key string) (*response.AppDTO, error) { + var appDTO response.AppDTO + app, err := appRepo.GetFirst(appRepo.WithKey(key)) + if err != nil { + return nil, err + } + appDTO.App = app + details, err := appDetailRepo.GetBy(appDetailRepo.WithAppId(app.ID)) + if err != nil { + return nil, err + } + var versionsRaw []string + for _, detail := range details { + versionsRaw = append(versionsRaw, detail.Version) + } + appDTO.Versions = common.GetSortedVersions(versionsRaw) + + return &appDTO, nil +} + +func (a AppService) GetAppDetail(appID uint, version, appType string) (response.AppDetailDTO, error) { + var ( + appDetailDTO response.AppDetailDTO + opts []repo.DBOption + ) + opts = append(opts, appDetailRepo.WithAppId(appID), appDetailRepo.WithVersion(version)) + detail, err := appDetailRepo.GetFirst(opts...) + if err != nil { + return appDetailDTO, err + } + appDetailDTO.AppDetail = detail + appDetailDTO.Enable = true + + if appType == "runtime" { + app, err := appRepo.GetFirst(commonRepo.WithByID(appID)) + if err != nil { + return appDetailDTO, err + } + fileOp := files.NewFileOp() + + versionPath := filepath.Join(app.GetAppResourcePath(), detail.Version) + if !fileOp.Stat(versionPath) || detail.Update { + if err = downloadApp(app, detail, nil); err != nil { + return appDetailDTO, err + } + } + switch app.Type { + case constant.RuntimePHP: + buildPath := filepath.Join(versionPath, "build") + paramsPath := filepath.Join(buildPath, "config.json") + if !fileOp.Stat(paramsPath) { + return appDetailDTO, buserr.New(constant.ErrFileNotExist) + } + param, err := fileOp.GetContent(paramsPath) + if err != nil { + return appDetailDTO, err + } + paramMap := make(map[string]interface{}) + if err := json.Unmarshal(param, ¶mMap); err != nil { + return appDetailDTO, err + } + appDetailDTO.Params = paramMap + composePath := filepath.Join(buildPath, "docker-compose.yml") + if !fileOp.Stat(composePath) { + return appDetailDTO, buserr.New(constant.ErrFileNotExist) + } + compose, err := fileOp.GetContent(composePath) + if err != nil { + return appDetailDTO, err + } + composeMap := make(map[string]interface{}) + if err := yaml.Unmarshal(compose, &composeMap); err != nil { + return appDetailDTO, err + } + if service, ok := composeMap["services"]; ok { + servicesMap := service.(map[string]interface{}) + for k := range servicesMap { + appDetailDTO.Image = k + } + } + } + } else { + paramMap := make(map[string]interface{}) + if err := json.Unmarshal([]byte(detail.Params), ¶mMap); err != nil { + return appDetailDTO, err + } + appDetailDTO.Params = paramMap + } + + if appDetailDTO.DockerCompose == "" { + filename := filepath.Base(appDetailDTO.DownloadUrl) + dockerComposeUrl := fmt.Sprintf("%s%s", strings.TrimSuffix(appDetailDTO.DownloadUrl, filename), "docker-compose.yml") + statusCode, composeRes, err := httpUtil.HandleGet(dockerComposeUrl, http.MethodGet, constant.TimeOut20s) + if err != nil { + return appDetailDTO, buserr.WithDetail("ErrGetCompose", err.Error(), err) + } + if statusCode > 200 { + return appDetailDTO, buserr.WithDetail("ErrGetCompose", string(composeRes), err) + } + detail.DockerCompose = string(composeRes) + _ = appDetailRepo.Update(context.Background(), detail) + appDetailDTO.DockerCompose = string(composeRes) + } + + appDetailDTO.HostMode = isHostModel(appDetailDTO.DockerCompose) + + app, err := appRepo.GetFirst(commonRepo.WithByID(detail.AppId)) + if err != nil { + return appDetailDTO, err + } + if err := checkLimit(app); err != nil { + appDetailDTO.Enable = false + } + return appDetailDTO, nil +} +func (a AppService) GetAppDetailByID(id uint) (*response.AppDetailDTO, error) { + res := &response.AppDetailDTO{} + appDetail, err := appDetailRepo.GetFirst(commonRepo.WithByID(id)) + if err != nil { + return nil, err + } + res.AppDetail = appDetail + paramMap := make(map[string]interface{}) + if err := json.Unmarshal([]byte(appDetail.Params), ¶mMap); err != nil { + return nil, err + } + res.Params = paramMap + res.HostMode = isHostModel(appDetail.DockerCompose) + return res, nil +} + +func (a AppService) GetIgnoredApp() ([]response.IgnoredApp, error) { + var res []response.IgnoredApp + details, _ := appDetailRepo.GetBy(appDetailRepo.WithIgnored()) + if len(details) == 0 { + return res, nil + } + for _, detail := range details { + app, err := appRepo.GetFirst(commonRepo.WithByID(detail.AppId)) + if err != nil { + return nil, err + } + res = append(res, response.IgnoredApp{ + Name: app.Name, + Version: detail.Version, + DetailID: detail.ID, + Icon: app.Icon, + }) + } + return res, nil +} + +func (a AppService) Install(ctx context.Context, req request.AppInstallCreate) (appInstall *model.AppInstall, err error) { + if err = docker.CreateDefaultDockerNetwork(); err != nil { + err = buserr.WithDetail(constant.Err1PanelNetworkFailed, err.Error(), nil) + return + } + if list, _ := appInstallRepo.ListBy(commonRepo.WithByName(req.Name)); len(list) > 0 { + err = buserr.New(constant.ErrAppNameExist) + return + } + var ( + httpPort int + httpsPort int + appDetail model.AppDetail + app model.App + ) + appDetail, err = appDetailRepo.GetFirst(commonRepo.WithByID(req.AppDetailId)) + if err != nil { + return + } + app, err = appRepo.GetFirst(commonRepo.WithByID(appDetail.AppId)) + if err != nil { + return + } + if DatabaseKeys[app.Key] > 0 { + if existDatabases, _ := databaseRepo.GetList(commonRepo.WithByName(req.Name)); len(existDatabases) > 0 { + err = buserr.New(constant.ErrRemoteExist) + return + } + } + for key := range req.Params { + if !strings.Contains(key, "PANEL_APP_PORT") { + continue + } + var port int + if port, err = checkPort(key, req.Params); err == nil { + if key == "PANEL_APP_PORT_HTTP" { + httpPort = port + } + if key == "PANEL_APP_PORT_HTTPS" { + httpsPort = port + } + } else { + return + } + } + + if err = checkRequiredAndLimit(app); err != nil { + return + } + + appInstall = &model.AppInstall{ + Name: req.Name, + AppId: appDetail.AppId, + AppDetailId: appDetail.ID, + Version: appDetail.Version, + Status: constant.Installing, + HttpPort: httpPort, + HttpsPort: httpsPort, + App: app, + } + composeMap := make(map[string]interface{}) + if req.EditCompose { + if err = yaml.Unmarshal([]byte(req.DockerCompose), &composeMap); err != nil { + return + } + } else { + if err = yaml.Unmarshal([]byte(appDetail.DockerCompose), &composeMap); err != nil { + return + } + } + + value, ok := composeMap["services"] + if !ok || value == nil { + err = buserr.New(constant.ErrFileParse) + return + } + servicesMap := value.(map[string]interface{}) + containerName := constant.ContainerPrefix + app.Key + "-" + common.RandStr(4) + if req.Advanced && req.ContainerName != "" { + containerName = req.ContainerName + appInstalls, _ := appInstallRepo.ListBy(appInstallRepo.WithContainerName(containerName)) + if len(appInstalls) > 0 { + err = buserr.New(constant.ErrContainerName) + return + } + containerExist := false + containerExist, err = checkContainerNameIsExist(req.ContainerName, appInstall.GetPath()) + if err != nil { + return + } + if containerExist { + err = buserr.New(constant.ErrContainerName) + return + } + } + req.Params[constant.ContainerName] = containerName + appInstall.ContainerName = containerName + + index := 0 + serviceName := "" + for k := range servicesMap { + serviceName = k + if index > 0 { + continue + } + index++ + } + if app.Limit == 0 && appInstall.Name != serviceName && len(servicesMap) == 1 { + servicesMap[appInstall.Name] = servicesMap[serviceName] + delete(servicesMap, serviceName) + serviceName = appInstall.Name + } + appInstall.ServiceName = serviceName + + if err = addDockerComposeCommonParam(composeMap, appInstall.ServiceName, req.AppContainerConfig, req.Params); err != nil { + return + } + var ( + composeByte []byte + paramByte []byte + ) + + composeByte, err = yaml.Marshal(composeMap) + if err != nil { + return + } + appInstall.DockerCompose = string(composeByte) + + defer func() { + if err != nil { + hErr := handleAppInstallErr(ctx, appInstall) + if hErr != nil { + global.LOG.Errorf("delete app dir error %s", hErr.Error()) + } + } + }() + if hostName, ok := req.Params["PANEL_DB_HOST"]; ok { + database, _ := databaseRepo.Get(commonRepo.WithByName(hostName.(string))) + if !reflect.DeepEqual(database, model.Database{}) { + req.Params["PANEL_DB_HOST"] = database.Address + req.Params["PANEL_DB_PORT"] = database.Port + req.Params["PANEL_DB_HOST_NAME"] = hostName + } + } + paramByte, err = json.Marshal(req.Params) + if err != nil { + return + } + appInstall.Env = string(paramByte) + + if err = appInstallRepo.Create(ctx, appInstall); err != nil { + return + } + if err = createLink(ctx, app, appInstall, req.Params); err != nil { + return + } + go func() { + defer func() { + if err != nil { + appInstall.Status = constant.UpErr + appInstall.Message = err.Error() + _ = appInstallRepo.Save(context.Background(), appInstall) + } + }() + if err = copyData(app, appDetail, appInstall, req); err != nil { + return + } + if err = runScript(appInstall, "init"); err != nil { + return + } + upApp(appInstall, req.PullImage) + }() + go updateToolApp(appInstall) + return +} + +func (a AppService) SyncAppListFromLocal() { + fileOp := files.NewFileOp() + localAppDir := constant.LocalAppResourceDir + if !fileOp.Stat(localAppDir) { + return + } + var ( + err error + dirEntries []os.DirEntry + localApps []model.App + ) + + defer func() { + if err != nil { + global.LOG.Errorf("Sync local app failed %v", err) + } + }() + + global.LOG.Infof("Starting local application synchronization ...") + dirEntries, err = os.ReadDir(localAppDir) + if err != nil { + return + } + for _, dirEntry := range dirEntries { + if dirEntry.IsDir() { + appDir := filepath.Join(localAppDir, dirEntry.Name()) + appDirEntries, err := os.ReadDir(appDir) + if err != nil { + global.LOG.Errorf(i18n.GetMsgWithMap("ErrAppDirNull", map[string]interface{}{"name": dirEntry.Name(), "err": err.Error()})) + continue + } + app, err := handleLocalApp(appDir) + if err != nil { + global.LOG.Errorf(i18n.GetMsgWithMap("LocalAppErr", map[string]interface{}{"name": dirEntry.Name(), "err": err.Error()})) + continue + } + var appDetails []model.AppDetail + for _, appDirEntry := range appDirEntries { + if appDirEntry.IsDir() { + appDetail := model.AppDetail{ + Version: appDirEntry.Name(), + Status: constant.AppNormal, + } + versionDir := filepath.Join(appDir, appDirEntry.Name()) + if err = handleLocalAppDetail(versionDir, &appDetail); err != nil { + global.LOG.Errorf(i18n.GetMsgWithMap("LocalAppVersionErr", map[string]interface{}{"name": app.Name, "version": appDetail.Version, "err": err.Error()})) + continue + } + appDetails = append(appDetails, appDetail) + } + } + if len(appDetails) > 0 { + app.Details = appDetails + localApps = append(localApps, *app) + } else { + global.LOG.Errorf(i18n.GetMsgWithMap("LocalAppVersionNull", map[string]interface{}{"name": app.Name})) + } + } + } + + var ( + newApps []model.App + deleteApps []model.App + updateApps []model.App + oldAppIds []uint + + deleteAppIds []uint + deleteAppDetails []model.AppDetail + newAppDetails []model.AppDetail + updateDetails []model.AppDetail + + appTags []*model.AppTag + ) + + oldApps, _ := appRepo.GetBy(appRepo.WithResource(constant.AppResourceLocal)) + apps := make(map[string]model.App, len(oldApps)) + for _, old := range oldApps { + old.Status = constant.AppTakeDown + apps[old.Key] = old + } + for _, app := range localApps { + if oldApp, ok := apps[app.Key]; ok { + app.ID = oldApp.ID + appDetails := make(map[string]model.AppDetail, len(oldApp.Details)) + for _, old := range oldApp.Details { + old.Status = constant.AppTakeDown + appDetails[old.Version] = old + } + for i, newDetail := range app.Details { + version := newDetail.Version + newDetail.Status = constant.AppNormal + newDetail.AppId = app.ID + oldDetail, exist := appDetails[version] + if exist { + newDetail.ID = oldDetail.ID + delete(appDetails, version) + } + app.Details[i] = newDetail + } + for _, v := range appDetails { + app.Details = append(app.Details, v) + } + } + app.TagsKey = append(app.TagsKey, constant.AppResourceLocal) + apps[app.Key] = app + } + + for _, app := range apps { + if app.ID == 0 { + newApps = append(newApps, app) + } else { + oldAppIds = append(oldAppIds, app.ID) + if app.Status == constant.AppTakeDown { + installs, _ := appInstallRepo.ListBy(appInstallRepo.WithAppId(app.ID)) + if len(installs) > 0 { + updateApps = append(updateApps, app) + continue + } + deleteAppIds = append(deleteAppIds, app.ID) + deleteApps = append(deleteApps, app) + deleteAppDetails = append(deleteAppDetails, app.Details...) + } else { + updateApps = append(updateApps, app) + } + } + + } + + tags, _ := tagRepo.All() + tagMap := make(map[string]uint, len(tags)) + for _, tag := range tags { + tagMap[tag.Key] = tag.ID + } + + tx, ctx := getTxAndContext() + defer tx.Rollback() + if len(newApps) > 0 { + if err = appRepo.BatchCreate(ctx, newApps); err != nil { + return + } + } + for _, update := range updateApps { + if err = appRepo.Save(ctx, &update); err != nil { + return + } + } + if len(deleteApps) > 0 { + if err = appRepo.BatchDelete(ctx, deleteApps); err != nil { + return + } + if err = appDetailRepo.DeleteByAppIds(ctx, deleteAppIds); err != nil { + return + } + } + + if err = appTagRepo.DeleteByAppIds(ctx, oldAppIds); err != nil { + return + } + for _, newApp := range newApps { + if newApp.ID > 0 { + for _, detail := range newApp.Details { + detail.AppId = newApp.ID + newAppDetails = append(newAppDetails, detail) + } + } + } + for _, update := range updateApps { + for _, detail := range update.Details { + if detail.ID == 0 { + detail.AppId = update.ID + newAppDetails = append(newAppDetails, detail) + } else { + if detail.Status == constant.AppNormal { + updateDetails = append(updateDetails, detail) + } else { + deleteAppDetails = append(deleteAppDetails, detail) + } + } + } + } + + allApps := append(newApps, updateApps...) + for _, app := range allApps { + for _, t := range app.TagsKey { + tagId, ok := tagMap[t] + if ok { + appTags = append(appTags, &model.AppTag{ + AppId: app.ID, + TagId: tagId, + }) + } + } + } + + if len(newAppDetails) > 0 { + if err = appDetailRepo.BatchCreate(ctx, newAppDetails); err != nil { + return + } + } + + for _, updateAppDetail := range updateDetails { + if err = appDetailRepo.Update(ctx, updateAppDetail); err != nil { + return + } + } + + if len(deleteAppDetails) > 0 { + if err = appDetailRepo.BatchDelete(ctx, deleteAppDetails); err != nil { + return + } + } + + if len(oldAppIds) > 0 { + if err = appTagRepo.DeleteByAppIds(ctx, oldAppIds); err != nil { + return + } + } + + if len(appTags) > 0 { + if err = appTagRepo.BatchCreate(ctx, appTags); err != nil { + return + } + } + tx.Commit() + global.LOG.Infof("Synchronization of local applications completed") +} + +func (a AppService) GetAppUpdate() (*response.AppUpdateRes, error) { + res := &response.AppUpdateRes{ + CanUpdate: false, + } + + versionUrl := fmt.Sprintf("%s/%s/1panel.json.version.txt", global.CONF.System.AppRepo, global.CONF.System.Mode) + _, versionRes, err := http2.HandleGet(versionUrl, http.MethodGet, constant.TimeOut20s) + if err != nil { + return nil, err + } + lastModifiedStr := string(versionRes) + lastModified, err := strconv.Atoi(lastModifiedStr) + if err != nil { + return nil, err + } + setting, err := NewISettingService().GetSettingInfo() + if err != nil { + return nil, err + } + if setting.AppStoreSyncStatus == constant.Syncing { + res.IsSyncing = true + return res, nil + } + + appStoreLastModified, _ := strconv.Atoi(setting.AppStoreLastModified) + res.AppStoreLastModified = appStoreLastModified + if setting.AppStoreLastModified == "" || lastModified != appStoreLastModified { + res.CanUpdate = true + return res, err + } + apps, _ := appRepo.GetBy(appRepo.WithResource(constant.AppResourceRemote)) + for _, app := range apps { + if app.Icon == "" { + res.CanUpdate = true + return res, err + } + } + + list, err := getAppList() + if err != nil { + return res, err + } + if list.Extra.Version != "" && setting.SystemVersion != list.Extra.Version && !common.CompareVersion(setting.SystemVersion, list.Extra.Version) { + global.LOG.Errorf("The current version is too low to synchronize with the App Store. The minimum required version is %s", list.Extra.Version) + return nil, buserr.New("ErrVersionTooLow") + } + res.AppList = list + return res, nil +} + +func getAppFromRepo(downloadPath string) error { + downloadUrl := downloadPath + global.LOG.Infof("[AppStore] download file from %s", downloadUrl) + fileOp := files.NewFileOp() + packagePath := filepath.Join(constant.ResourceDir, filepath.Base(downloadUrl)) + if err := fileOp.DownloadFileWithProxy(downloadUrl, packagePath); err != nil { + return err + } + if err := fileOp.Decompress(packagePath, constant.ResourceDir, files.SdkZip, ""); err != nil { + return err + } + defer func() { + _ = fileOp.DeleteFile(packagePath) + }() + return nil +} + +func getAppList() (*dto.AppList, error) { + list := &dto.AppList{} + if err := getAppFromRepo(fmt.Sprintf("%s/%s/1panel.json.zip", global.CONF.System.AppRepo, global.CONF.System.Mode)); err != nil { + return nil, err + } + listFile := filepath.Join(constant.ResourceDir, "1panel.json") + content, err := os.ReadFile(listFile) + if err != nil { + return nil, err + } + if err = json.Unmarshal(content, list); err != nil { + return nil, err + } + return list, nil +} + +var InitTypes = map[string]struct{}{ + "runtime": {}, + "php": {}, + "node": {}, +} + +func (a AppService) SyncAppListFromRemote() (err error) { + global.LOG.Infof("Starting synchronization with App Store...") + updateRes, err := a.GetAppUpdate() + if err != nil { + return err + } + if !updateRes.CanUpdate { + if updateRes.IsSyncing { + global.LOG.Infof("AppStore is Syncing!") + return + } + global.LOG.Infof("The App Store is at the latest version") + return + } + + list := &dto.AppList{} + if updateRes.AppList == nil { + list, err = getAppList() + if err != nil { + return + } + } else { + list = updateRes.AppList + } + settingService := NewISettingService() + _ = settingService.Update("AppStoreSyncStatus", constant.Syncing) + + var ( + tags []*model.Tag + appTags []*model.AppTag + oldAppIds []uint + ) + for _, t := range list.Extra.Tags { + tags = append(tags, &model.Tag{ + Key: t.Key, + Name: t.Name, + Sort: t.Sort, + }) + } + oldApps, err := appRepo.GetBy(appRepo.WithResource(constant.AppResourceRemote)) + if err != nil { + return + } + for _, old := range oldApps { + oldAppIds = append(oldAppIds, old.ID) + } + + transport := xpack.LoadRequestTransport() + baseRemoteUrl := fmt.Sprintf("%s/%s/1panel", global.CONF.System.AppRepo, global.CONF.System.Mode) + appsMap := getApps(oldApps, list.Apps) + + global.LOG.Infof("Starting synchronization of application details...") + for _, l := range list.Apps { + app := appsMap[l.AppProperty.Key] + _, iconRes, err := httpUtil.HandleGetWithTransport(l.Icon, http.MethodGet, transport, constant.TimeOut20s) + if err != nil { + return err + } + iconStr := "" + if !strings.Contains(string(iconRes), "") { + iconStr = base64.StdEncoding.EncodeToString(iconRes) + } + + app.Icon = iconStr + app.TagsKey = l.AppProperty.Tags + if l.AppProperty.Recommend > 0 { + app.Recommend = l.AppProperty.Recommend + } else { + app.Recommend = 9999 + } + app.ReadMe = l.ReadMe + app.LastModified = l.LastModified + versions := l.Versions + detailsMap := getAppDetails(app.Details, versions) + for _, v := range versions { + version := v.Name + detail := detailsMap[version] + versionUrl := fmt.Sprintf("%s/%s/%s", baseRemoteUrl, app.Key, version) + + if _, ok := InitTypes[app.Type]; ok { + dockerComposeUrl := fmt.Sprintf("%s/%s", versionUrl, "docker-compose.yml") + _, composeRes, err := httpUtil.HandleGetWithTransport(dockerComposeUrl, http.MethodGet, transport, constant.TimeOut20s) + if err != nil { + return err + } + detail.DockerCompose = string(composeRes) + } else { + detail.DockerCompose = "" + } + + paramByte, _ := json.Marshal(v.AppForm) + detail.Params = string(paramByte) + detail.DownloadUrl = fmt.Sprintf("%s/%s", versionUrl, app.Key+"-"+version+".tar.gz") + detail.DownloadCallBackUrl = v.DownloadCallBackUrl + detail.Update = true + detail.LastModified = v.LastModified + detailsMap[version] = detail + } + var newDetails []model.AppDetail + for _, detail := range detailsMap { + newDetails = append(newDetails, detail) + } + app.Details = newDetails + appsMap[l.AppProperty.Key] = app + } + + global.LOG.Infof("Synchronization of application details Success") + + var ( + addAppArray []model.App + updateAppArray []model.App + deleteAppArray []model.App + deleteIds []uint + tagMap = make(map[string]uint, len(tags)) + ) + + for _, v := range appsMap { + if v.ID == 0 { + addAppArray = append(addAppArray, v) + } else { + if v.Status == constant.AppTakeDown { + installs, _ := appInstallRepo.ListBy(appInstallRepo.WithAppId(v.ID)) + if len(installs) > 0 { + updateAppArray = append(updateAppArray, v) + continue + } + deleteAppArray = append(deleteAppArray, v) + deleteIds = append(deleteIds, v.ID) + } else { + updateAppArray = append(updateAppArray, v) + } + } + } + tx, ctx := getTxAndContext() + defer tx.Rollback() + if len(addAppArray) > 0 { + if err = appRepo.BatchCreate(ctx, addAppArray); err != nil { + return + } + } + if len(deleteAppArray) > 0 { + if err = appRepo.BatchDelete(ctx, deleteAppArray); err != nil { + return + } + if err = appDetailRepo.DeleteByAppIds(ctx, deleteIds); err != nil { + return + } + } + if err = tagRepo.DeleteAll(ctx); err != nil { + return + } + if len(tags) > 0 { + if err = tagRepo.BatchCreate(ctx, tags); err != nil { + return + } + for _, t := range tags { + tagMap[t.Key] = t.ID + } + } + for _, update := range updateAppArray { + if err = appRepo.Save(ctx, &update); err != nil { + return + } + } + apps := append(addAppArray, updateAppArray...) + + var ( + addDetails []model.AppDetail + updateDetails []model.AppDetail + deleteDetails []model.AppDetail + ) + for _, app := range apps { + for _, t := range app.TagsKey { + tagId, ok := tagMap[t] + if ok { + appTags = append(appTags, &model.AppTag{ + AppId: app.ID, + TagId: tagId, + }) + } + } + for _, d := range app.Details { + d.AppId = app.ID + if d.ID == 0 { + addDetails = append(addDetails, d) + } else { + if d.Status == constant.AppTakeDown { + runtime, _ := runtimeRepo.GetFirst(runtimeRepo.WithDetailId(d.ID)) + if runtime != nil { + updateDetails = append(updateDetails, d) + continue + } + installs, _ := appInstallRepo.ListBy(appInstallRepo.WithDetailIdsIn([]uint{d.ID})) + if len(installs) > 0 { + updateDetails = append(updateDetails, d) + continue + } + deleteDetails = append(deleteDetails, d) + } else { + updateDetails = append(updateDetails, d) + } + } + } + } + if len(addDetails) > 0 { + if err = appDetailRepo.BatchCreate(ctx, addDetails); err != nil { + return + } + } + if len(deleteDetails) > 0 { + if err = appDetailRepo.BatchDelete(ctx, deleteDetails); err != nil { + return + } + } + for _, u := range updateDetails { + if err = appDetailRepo.Update(ctx, u); err != nil { + return + } + } + + if len(oldAppIds) > 0 { + if err = appTagRepo.DeleteByAppIds(ctx, oldAppIds); err != nil { + return + } + } + + if len(appTags) > 0 { + if err = appTagRepo.BatchCreate(ctx, appTags); err != nil { + return + } + } + tx.Commit() + + _ = settingService.Update("AppStoreSyncStatus", constant.SyncSuccess) + _ = settingService.Update("AppStoreLastModified", strconv.Itoa(list.LastModified)) + + global.LOG.Infof("Synchronization with the App Store was successful!") + return +} diff --git a/agent/app/service/app_install.go b/agent/app/service/app_install.go new file mode 100644 index 000000000..c3f0b0ffd --- /dev/null +++ b/agent/app/service/app_install.go @@ -0,0 +1,848 @@ +package service + +import ( + "context" + "encoding/json" + "fmt" + "math" + "net/http" + "os" + "path" + "path/filepath" + "reflect" + "sort" + "strconv" + "strings" + + "github.com/1Panel-dev/1Panel/agent/utils/files" + httpUtil "github.com/1Panel-dev/1Panel/agent/utils/http" + "github.com/docker/docker/api/types" + "gopkg.in/yaml.v3" + + "github.com/1Panel-dev/1Panel/agent/utils/env" + "github.com/1Panel-dev/1Panel/agent/utils/nginx" + "github.com/joho/godotenv" + + "github.com/1Panel-dev/1Panel/agent/app/dto/request" + "github.com/1Panel-dev/1Panel/agent/app/dto/response" + "github.com/1Panel-dev/1Panel/agent/buserr" + + "github.com/1Panel-dev/1Panel/agent/app/repo" + + "github.com/1Panel-dev/1Panel/agent/app/dto" + "github.com/1Panel-dev/1Panel/agent/app/model" + "github.com/1Panel-dev/1Panel/agent/constant" + "github.com/1Panel-dev/1Panel/agent/global" + "github.com/1Panel-dev/1Panel/agent/utils/common" + "github.com/1Panel-dev/1Panel/agent/utils/compose" + "github.com/1Panel-dev/1Panel/agent/utils/docker" + "github.com/pkg/errors" +) + +type AppInstallService struct { +} + +type IAppInstallService interface { + Page(req request.AppInstalledSearch) (int64, []response.AppInstallDTO, error) + CheckExist(req request.AppInstalledInfo) (*response.AppInstalledCheck, error) + LoadPort(req dto.OperationWithNameAndType) (int64, error) + LoadConnInfo(req dto.OperationWithNameAndType) (response.DatabaseConn, error) + SearchForWebsite(req request.AppInstalledSearch) ([]response.AppInstallDTO, error) + Operate(req request.AppInstalledOperate) error + Update(req request.AppInstalledUpdate) error + IgnoreUpgrade(req request.AppInstalledIgnoreUpgrade) error + SyncAll(systemInit bool) error + GetServices(key string) ([]response.AppService, error) + GetUpdateVersions(req request.AppUpdateVersion) ([]dto.AppVersion, error) + GetParams(id uint) (*response.AppConfig, error) + ChangeAppPort(req request.PortUpdate) error + GetDefaultConfigByKey(key, name string) (string, error) + DeleteCheck(installId uint) ([]dto.AppResource, error) + + GetInstallList() ([]dto.AppInstallInfo, error) +} + +func NewIAppInstalledService() IAppInstallService { + return &AppInstallService{} +} + +func (a *AppInstallService) GetInstallList() ([]dto.AppInstallInfo, error) { + var datas []dto.AppInstallInfo + appInstalls, err := appInstallRepo.ListBy() + if err != nil { + return nil, err + } + for _, install := range appInstalls { + datas = append(datas, dto.AppInstallInfo{ID: install.ID, Key: install.App.Key, Name: install.Name}) + } + return datas, nil +} + +func (a *AppInstallService) Page(req request.AppInstalledSearch) (int64, []response.AppInstallDTO, error) { + var ( + opts []repo.DBOption + total int64 + installs []model.AppInstall + err error + ) + + if req.Name != "" { + opts = append(opts, commonRepo.WithLikeName(req.Name)) + } + if len(req.Tags) != 0 { + tags, err := tagRepo.GetByKeys(req.Tags) + if err != nil { + return 0, nil, err + } + var tagIds []uint + for _, t := range tags { + tagIds = append(tagIds, t.ID) + } + appTags, err := appTagRepo.GetByTagIds(tagIds) + if err != nil { + return 0, nil, err + } + var appIds []uint + for _, t := range appTags { + appIds = append(appIds, t.AppId) + } + + opts = append(opts, appInstallRepo.WithAppIdsIn(appIds)) + } + + if req.Update { + installs, err = appInstallRepo.ListBy(opts...) + if err != nil { + return 0, nil, err + } + } else { + total, installs, err = appInstallRepo.Page(req.Page, req.PageSize, opts...) + if err != nil { + return 0, nil, err + } + } + + installDTOs, err := handleInstalled(installs, req.Update, req.Sync) + if err != nil { + return 0, nil, err + } + if req.Update { + total = int64(len(installDTOs)) + } + + return total, installDTOs, nil +} + +func (a *AppInstallService) CheckExist(req request.AppInstalledInfo) (*response.AppInstalledCheck, error) { + res := &response.AppInstalledCheck{ + IsExist: false, + } + + app, err := appRepo.GetFirst(appRepo.WithKey(req.Key)) + if err != nil { + return res, nil + } + res.App = app.Name + + var appInstall model.AppInstall + if len(req.Name) == 0 { + appInstall, _ = appInstallRepo.GetFirst(appInstallRepo.WithAppId(app.ID)) + } else { + appInstall, _ = appInstallRepo.GetFirst(appInstallRepo.WithAppId(app.ID), commonRepo.WithByName(req.Name)) + } + + if reflect.DeepEqual(appInstall, model.AppInstall{}) { + return res, nil + } + if err = syncAppInstallStatus(&appInstall, false); err != nil { + return nil, err + } + + res.ContainerName = appInstall.ContainerName + res.Name = appInstall.Name + res.Version = appInstall.Version + res.CreatedAt = appInstall.CreatedAt + res.Status = appInstall.Status + res.AppInstallID = appInstall.ID + res.IsExist = true + res.InstallPath = path.Join(constant.AppInstallDir, appInstall.App.Key, appInstall.Name) + res.HttpPort = appInstall.HttpPort + res.HttpsPort = appInstall.HttpsPort + + return res, nil +} + +func (a *AppInstallService) LoadPort(req dto.OperationWithNameAndType) (int64, error) { + app, err := appInstallRepo.LoadBaseInfo(req.Type, req.Name) + if err != nil { + return int64(0), nil + } + return app.Port, nil +} + +func (a *AppInstallService) LoadConnInfo(req dto.OperationWithNameAndType) (response.DatabaseConn, error) { + var data response.DatabaseConn + app, err := appInstallRepo.LoadBaseInfo(req.Type, req.Name) + if err != nil { + return data, nil + } + data.Status = app.Status + data.Username = app.UserName + data.Password = app.Password + data.ServiceName = app.ServiceName + data.Port = app.Port + data.ContainerName = app.ContainerName + return data, nil +} + +func (a *AppInstallService) SearchForWebsite(req request.AppInstalledSearch) ([]response.AppInstallDTO, error) { + var ( + installs []model.AppInstall + err error + opts []repo.DBOption + ) + if req.Type != "" { + apps, err := appRepo.GetBy(appRepo.WithType(req.Type)) + if err != nil { + return nil, err + } + var ids []uint + for _, app := range apps { + ids = append(ids, app.ID) + } + if req.Unused { + opts = append(opts, appInstallRepo.WithIdNotInWebsite()) + } + opts = append(opts, appInstallRepo.WithAppIdsIn(ids)) + installs, err = appInstallRepo.ListBy(opts...) + if err != nil { + return nil, err + } + } else { + installs, err = appInstallRepo.ListBy() + if err != nil { + return nil, err + } + } + + return handleInstalled(installs, false, true) +} + +func (a *AppInstallService) Operate(req request.AppInstalledOperate) error { + install, err := appInstallRepo.GetFirstByCtx(context.Background(), commonRepo.WithByID(req.InstallId)) + if err != nil { + return err + } + if !req.ForceDelete && !files.NewFileOp().Stat(install.GetPath()) { + return buserr.New(constant.ErrInstallDirNotFound) + } + dockerComposePath := install.GetComposePath() + switch req.Operate { + case constant.Rebuild: + return rebuildApp(install) + case constant.Start: + out, err := compose.Start(dockerComposePath) + if err != nil { + return handleErr(install, err, out) + } + return syncAppInstallStatus(&install, false) + case constant.Stop: + out, err := compose.Stop(dockerComposePath) + if err != nil { + return handleErr(install, err, out) + } + return syncAppInstallStatus(&install, false) + case constant.Restart: + out, err := compose.Restart(dockerComposePath) + if err != nil { + return handleErr(install, err, out) + } + return syncAppInstallStatus(&install, false) + case constant.Delete: + if err := deleteAppInstall(install, req.DeleteBackup, req.ForceDelete, req.DeleteDB); err != nil && !req.ForceDelete { + return err + } + return nil + case constant.Sync: + return syncAppInstallStatus(&install, true) + case constant.Upgrade: + upgradeReq := request.AppInstallUpgrade{ + InstallID: install.ID, + DetailID: req.DetailId, + Backup: req.Backup, + PullImage: req.PullImage, + DockerCompose: req.DockerCompose, + } + return upgradeInstall(upgradeReq) + case constant.Reload: + return opNginx(install.ContainerName, constant.NginxReload) + default: + return errors.New("operate not support") + } +} + +func (a *AppInstallService) Update(req request.AppInstalledUpdate) error { + installed, err := appInstallRepo.GetFirst(commonRepo.WithByID(req.InstallId)) + if err != nil { + return err + } + changePort := false + port, ok := req.Params["PANEL_APP_PORT_HTTP"] + if ok { + portN := int(math.Ceil(port.(float64))) + if portN != installed.HttpPort { + changePort = true + httpPort, err := checkPort("PANEL_APP_PORT_HTTP", req.Params) + if err != nil { + return err + } + installed.HttpPort = httpPort + } + } + ports, ok := req.Params["PANEL_APP_PORT_HTTPS"] + if ok { + portN := int(math.Ceil(ports.(float64))) + if portN != installed.HttpsPort { + httpsPort, err := checkPort("PANEL_APP_PORT_HTTPS", req.Params) + if err != nil { + return err + } + installed.HttpsPort = httpsPort + } + } + + backupDockerCompose := installed.DockerCompose + if req.Advanced { + composeMap := make(map[string]interface{}) + if req.EditCompose { + if err = yaml.Unmarshal([]byte(req.DockerCompose), &composeMap); err != nil { + return err + } + } else { + if err = yaml.Unmarshal([]byte(installed.DockerCompose), &composeMap); err != nil { + return err + } + } + if err = addDockerComposeCommonParam(composeMap, installed.ServiceName, req.AppContainerConfig, req.Params); err != nil { + return err + } + composeByte, err := yaml.Marshal(composeMap) + if err != nil { + return err + } + installed.DockerCompose = string(composeByte) + if req.ContainerName == "" { + req.Params[constant.ContainerName] = installed.ContainerName + } else { + req.Params[constant.ContainerName] = req.ContainerName + if installed.ContainerName != req.ContainerName { + exist, _ := appInstallRepo.GetFirst(appInstallRepo.WithContainerName(req.ContainerName), appInstallRepo.WithIDNotIs(installed.ID)) + if exist.ID > 0 { + return buserr.New(constant.ErrContainerName) + } + containerExist, err := checkContainerNameIsExist(req.ContainerName, installed.GetPath()) + if err != nil { + return err + } + if containerExist { + return buserr.New(constant.ErrContainerName) + } + installed.ContainerName = req.ContainerName + } + } + } + + envPath := path.Join(installed.GetPath(), ".env") + oldEnvMaps, err := godotenv.Read(envPath) + if err != nil { + return err + } + backupEnvMaps := oldEnvMaps + handleMap(req.Params, oldEnvMaps) + paramByte, err := json.Marshal(oldEnvMaps) + if err != nil { + return err + } + installed.Env = string(paramByte) + if err := env.Write(oldEnvMaps, envPath); err != nil { + return err + } + fileOp := files.NewFileOp() + _ = fileOp.WriteFile(installed.GetComposePath(), strings.NewReader(installed.DockerCompose), 0755) + if err := rebuildApp(installed); err != nil { + _ = env.Write(backupEnvMaps, envPath) + _ = fileOp.WriteFile(installed.GetComposePath(), strings.NewReader(backupDockerCompose), 0755) + return err + } + installed.Status = constant.Running + _ = appInstallRepo.Save(context.Background(), &installed) + + website, _ := websiteRepo.GetFirst(websiteRepo.WithAppInstallId(installed.ID)) + if changePort && website.ID != 0 && website.Status == constant.Running { + go func() { + nginxInstall, err := getNginxFull(&website) + if err != nil { + global.LOG.Errorf(buserr.WithErr(constant.ErrUpdateBuWebsite, err).Error()) + return + } + config := nginxInstall.SiteConfig.Config + servers := config.FindServers() + if len(servers) == 0 { + global.LOG.Errorf(buserr.WithErr(constant.ErrUpdateBuWebsite, errors.New("nginx config is not valid")).Error()) + return + } + server := servers[0] + proxy := fmt.Sprintf("http://127.0.0.1:%d", installed.HttpPort) + server.UpdateRootProxy([]string{proxy}) + + if err := nginx.WriteConfig(config, nginx.IndentedStyle); err != nil { + global.LOG.Errorf(buserr.WithErr(constant.ErrUpdateBuWebsite, err).Error()) + return + } + if err := nginxCheckAndReload(nginxInstall.SiteConfig.OldContent, config.FilePath, nginxInstall.Install.ContainerName); err != nil { + global.LOG.Errorf(buserr.WithErr(constant.ErrUpdateBuWebsite, err).Error()) + return + } + }() + } + return nil +} + +func (a *AppInstallService) IgnoreUpgrade(req request.AppInstalledIgnoreUpgrade) error { + appDetail, err := appDetailRepo.GetFirst(commonRepo.WithByID(req.DetailID)) + if err != nil { + return err + } + appDetail.IgnoreUpgrade = req.Operate == "ignore" + return appDetailRepo.Update(context.Background(), appDetail) +} + +func (a *AppInstallService) SyncAll(systemInit bool) error { + allList, err := appInstallRepo.ListBy() + if err != nil { + return err + } + for _, i := range allList { + if i.Status == constant.Installing || i.Status == constant.Upgrading || i.Status == constant.Rebuilding { + if systemInit { + i.Status = constant.Error + i.Message = "1Panel restart causes the task to terminate" + _ = appInstallRepo.Save(context.Background(), &i) + } + continue + } + if !systemInit { + if err = syncAppInstallStatus(&i, false); err != nil { + global.LOG.Errorf("sync install app[%s] error,mgs: %s", i.Name, err.Error()) + } + } + } + return nil +} + +func (a *AppInstallService) GetServices(key string) ([]response.AppService, error) { + var res []response.AppService + if DatabaseKeys[key] > 0 { + if key == constant.AppPostgres { + key = constant.AppPostgresql + } + dbs, _ := databaseRepo.GetList(commonRepo.WithByType(key)) + if len(dbs) == 0 { + return res, nil + } + for _, db := range dbs { + service := response.AppService{ + Label: db.Name, + Value: db.Name, + } + if db.AppInstallID > 0 { + install, err := appInstallRepo.GetFirst(commonRepo.WithByID(db.AppInstallID)) + if err != nil { + return nil, err + } + paramMap := make(map[string]string) + if install.Param != "" { + _ = json.Unmarshal([]byte(install.Param), ¶mMap) + } + service.Config = paramMap + service.From = constant.AppResourceLocal + } else { + service.From = constant.AppResourceRemote + } + res = append(res, service) + } + } else { + app, err := appRepo.GetFirst(appRepo.WithKey(key)) + if err != nil { + return nil, err + } + installs, err := appInstallRepo.ListBy(appInstallRepo.WithAppId(app.ID), appInstallRepo.WithStatus(constant.Running)) + if err != nil { + return nil, err + } + for _, install := range installs { + paramMap := make(map[string]string) + if install.Param != "" { + _ = json.Unmarshal([]byte(install.Param), ¶mMap) + } + res = append(res, response.AppService{ + Label: install.Name, + Value: install.ServiceName, + Config: paramMap, + }) + } + } + return res, nil +} + +func (a *AppInstallService) GetUpdateVersions(req request.AppUpdateVersion) ([]dto.AppVersion, error) { + install, err := appInstallRepo.GetFirst(commonRepo.WithByID(req.AppInstallID)) + var versions []dto.AppVersion + if err != nil { + return versions, err + } + app, err := appRepo.GetFirst(commonRepo.WithByID(install.AppId)) + if err != nil { + return versions, err + } + details, err := appDetailRepo.GetBy(appDetailRepo.WithAppId(app.ID)) + if err != nil { + return versions, err + } + for _, detail := range details { + if detail.IgnoreUpgrade { + continue + } + if common.IsCrossVersion(install.Version, detail.Version) && !app.CrossVersionUpdate { + continue + } + if common.CompareVersion(detail.Version, install.Version) { + var newCompose string + if req.UpdateVersion != "" && req.UpdateVersion == detail.Version && detail.DockerCompose == "" && !app.IsLocalApp() { + filename := filepath.Base(detail.DownloadUrl) + dockerComposeUrl := fmt.Sprintf("%s%s", strings.TrimSuffix(detail.DownloadUrl, filename), "docker-compose.yml") + statusCode, composeRes, err := httpUtil.HandleGet(dockerComposeUrl, http.MethodGet, constant.TimeOut20s) + if err != nil { + return versions, err + } + if statusCode > 200 { + return versions, err + } + detail.DockerCompose = string(composeRes) + _ = appDetailRepo.Update(context.Background(), detail) + } + newCompose, err = getUpgradeCompose(install, detail) + if err != nil { + return versions, err + } + versions = append(versions, dto.AppVersion{ + Version: detail.Version, + DetailId: detail.ID, + DockerCompose: newCompose, + }) + } + } + sort.Slice(versions, func(i, j int) bool { + return common.CompareVersion(versions[i].Version, versions[j].Version) + }) + return versions, nil +} + +func (a *AppInstallService) ChangeAppPort(req request.PortUpdate) error { + if common.ScanPort(int(req.Port)) { + return buserr.WithDetail(constant.ErrPortInUsed, req.Port, nil) + } + + appInstall, err := appInstallRepo.LoadBaseInfo(req.Key, req.Name) + if err != nil { + return nil + } + + if err := updateInstallInfoInDB(req.Key, req.Name, "port", strconv.FormatInt(req.Port, 10)); err != nil { + return nil + } + + appRess, _ := appInstallResourceRepo.GetBy(appInstallResourceRepo.WithLinkId(appInstall.ID)) + for _, appRes := range appRess { + appInstall, err := appInstallRepo.GetFirst(commonRepo.WithByID(appRes.AppInstallId)) + if err != nil { + return err + } + if _, err := compose.Restart(fmt.Sprintf("%s/%s/%s/docker-compose.yml", constant.AppInstallDir, appInstall.App.Key, appInstall.Name)); err != nil { + global.LOG.Errorf("docker-compose restart %s[%s] failed, err: %v", appInstall.App.Key, appInstall.Name, err) + } + } + + return nil +} + +func (a *AppInstallService) DeleteCheck(installID uint) ([]dto.AppResource, error) { + var res []dto.AppResource + appInstall, err := appInstallRepo.GetFirst(commonRepo.WithByID(installID)) + if err != nil { + return nil, err + } + app, err := appRepo.GetFirst(commonRepo.WithByID(appInstall.AppId)) + if err != nil { + return nil, err + } + websites, _ := websiteRepo.GetBy(websiteRepo.WithAppInstallId(appInstall.ID)) + for _, website := range websites { + res = append(res, dto.AppResource{ + Type: "website", + Name: website.PrimaryDomain, + }) + } + if app.Key == constant.AppOpenresty { + websites, _ := websiteRepo.GetBy() + for _, website := range websites { + res = append(res, dto.AppResource{ + Type: "website", + Name: website.PrimaryDomain, + }) + } + } + if app.Type == constant.Runtime { + resources, _ := appInstallResourceRepo.GetBy(appInstallResourceRepo.WithLinkId(appInstall.ID), commonRepo.WithByFrom(constant.AppResourceLocal)) + for _, resource := range resources { + linkInstall, _ := appInstallRepo.GetFirst(commonRepo.WithByID(resource.AppInstallId)) + res = append(res, dto.AppResource{ + Type: "app", + Name: linkInstall.Name, + }) + } + } + return res, nil +} + +func (a *AppInstallService) GetDefaultConfigByKey(key, name string) (string, error) { + baseInfo, err := appInstallRepo.LoadBaseInfo(key, name) + if err != nil { + return "", err + } + + fileOp := files.NewFileOp() + filePath := path.Join(constant.AppResourceDir, "remote", baseInfo.Key, baseInfo.Version, "conf") + if !fileOp.Stat(filePath) { + filePath = path.Join(constant.AppResourceDir, baseInfo.Key, "versions", baseInfo.Version, "conf") + } + if !fileOp.Stat(filePath) { + return "", buserr.New(constant.ErrPathNotFound) + } + + if key == constant.AppMysql || key == constant.AppMariaDB { + filePath = path.Join(filePath, "my.cnf") + } + if key == constant.AppRedis { + filePath = path.Join(filePath, "redis.conf") + } + if key == constant.AppOpenresty { + filePath = path.Join(filePath, "nginx.conf") + } + contentByte, err := os.ReadFile(filePath) + if err != nil { + return "", err + } + return string(contentByte), nil +} + +func (a *AppInstallService) GetParams(id uint) (*response.AppConfig, error) { + var ( + params []response.AppParam + appForm dto.AppForm + envs = make(map[string]interface{}) + res response.AppConfig + ) + install, err := appInstallRepo.GetFirst(commonRepo.WithByID(id)) + if err != nil { + return nil, err + } + detail, err := appDetailRepo.GetFirst(commonRepo.WithByID(install.AppDetailId)) + if err != nil { + return nil, err + } + if err = json.Unmarshal([]byte(detail.Params), &appForm); err != nil { + return nil, err + } + if err = json.Unmarshal([]byte(install.Env), &envs); err != nil { + return nil, err + } + for _, form := range appForm.FormFields { + if v, ok := envs[form.EnvKey]; ok { + appParam := response.AppParam{ + Edit: false, + Key: form.EnvKey, + Rule: form.Rule, + Type: form.Type, + Multiple: form.Multiple, + } + if form.Edit { + appParam.Edit = true + } + appParam.LabelZh = form.LabelZh + appParam.LabelEn = form.LabelEn + appParam.Value = v + if form.Type == "service" { + appInstall, _ := appInstallRepo.GetFirst(appInstallRepo.WithServiceName(v.(string))) + appParam.ShowValue = appInstall.Name + } else if form.Type == "select" { + if form.Multiple { + if v == "" { + appParam.Value = []string{} + } else { + if str, ok := v.(string); ok { + appParam.Value = strings.Split(str, ",") + } + } + } else { + for _, fv := range form.Values { + if fv.Value == v { + appParam.ShowValue = fv.Label + break + } + } + } + appParam.Values = form.Values + } + params = append(params, appParam) + } else { + params = append(params, response.AppParam{ + Edit: form.Edit, + Key: form.EnvKey, + Rule: form.Rule, + Type: form.Type, + LabelZh: form.LabelZh, + LabelEn: form.LabelEn, + Value: form.Default, + Values: form.Values, + Multiple: form.Multiple, + }) + } + } + + config := getAppCommonConfig(envs) + config.DockerCompose = install.DockerCompose + res.Params = params + if config.ContainerName == "" { + config.ContainerName = install.ContainerName + } + res.AppContainerConfig = config + res.HostMode = isHostModel(install.DockerCompose) + return &res, nil +} + +func syncAppInstallStatus(appInstall *model.AppInstall, force bool) error { + if appInstall.Status == constant.Installing || appInstall.Status == constant.Rebuilding || appInstall.Status == constant.Upgrading { + return nil + } + cli, err := docker.NewClient() + if err != nil { + return err + } + defer cli.Close() + + var ( + containers []types.Container + containersMap map[string]types.Container + containerNames = strings.Split(appInstall.ContainerName, ",") + ) + containers, err = cli.ListContainersByName(containerNames) + if err != nil { + return err + } + containersMap = make(map[string]types.Container) + for _, con := range containers { + containersMap[con.Names[0]] = con + } + synAppInstall(containersMap, appInstall, force) + return nil +} + +func updateInstallInfoInDB(appKey, appName, param string, value interface{}) error { + if param != "password" && param != "port" && param != "user-password" { + return nil + } + appInstall, err := appInstallRepo.LoadBaseInfo(appKey, appName) + if err != nil { + return nil + } + envPath := fmt.Sprintf("%s/%s/.env", appInstall.AppPath, appInstall.Name) + lineBytes, err := os.ReadFile(envPath) + if err != nil { + return err + } + + envKey := "" + switch param { + case "password": + if appKey == "mysql" || appKey == "mariadb" || appKey == "postgresql" { + envKey = "PANEL_DB_ROOT_PASSWORD=" + } else { + envKey = "PANEL_REDIS_ROOT_PASSWORD=" + } + case "port": + envKey = "PANEL_APP_PORT_HTTP=" + default: + envKey = "PANEL_DB_USER_PASSWORD=" + } + files := strings.Split(string(lineBytes), "\n") + var newFiles []string + for _, line := range files { + if strings.HasPrefix(line, envKey) { + newFiles = append(newFiles, fmt.Sprintf("%s%v", envKey, value)) + } else { + newFiles = append(newFiles, line) + } + } + file, err := os.OpenFile(envPath, os.O_WRONLY|os.O_TRUNC, 0666) + if err != nil { + return err + } + defer file.Close() + _, err = file.WriteString(strings.Join(newFiles, "\n")) + if err != nil { + return err + } + + oldVal, newVal := "", "" + if param == "password" { + oldVal = fmt.Sprintf("\"PANEL_DB_ROOT_PASSWORD\":\"%v\"", appInstall.Password) + newVal = fmt.Sprintf("\"PANEL_DB_ROOT_PASSWORD\":\"%v\"", value) + if appKey == "redis" { + oldVal = fmt.Sprintf("\"PANEL_REDIS_ROOT_PASSWORD\":\"%v\"", appInstall.Password) + newVal = fmt.Sprintf("\"PANEL_REDIS_ROOT_PASSWORD\":\"%v\"", value) + } + _ = appInstallRepo.BatchUpdateBy(map[string]interface{}{ + "param": strings.ReplaceAll(appInstall.Param, oldVal, newVal), + "env": strings.ReplaceAll(appInstall.Env, oldVal, newVal), + }, commonRepo.WithByID(appInstall.ID)) + } + if param == "user-password" { + oldVal = fmt.Sprintf("\"PANEL_DB_USER_PASSWORD\":\"%v\"", appInstall.UserPassword) + newVal = fmt.Sprintf("\"PANEL_DB_USER_PASSWORD\":\"%v\"", value) + _ = appInstallRepo.BatchUpdateBy(map[string]interface{}{ + "param": strings.ReplaceAll(appInstall.Param, oldVal, newVal), + "env": strings.ReplaceAll(appInstall.Env, oldVal, newVal), + }, commonRepo.WithByID(appInstall.ID)) + } + if param == "port" { + oldVal = fmt.Sprintf("\"PANEL_APP_PORT_HTTP\":%v", appInstall.Port) + newVal = fmt.Sprintf("\"PANEL_APP_PORT_HTTP\":%v", value) + _ = appInstallRepo.BatchUpdateBy(map[string]interface{}{ + "param": strings.ReplaceAll(appInstall.Param, oldVal, newVal), + "env": strings.ReplaceAll(appInstall.Env, oldVal, newVal), + "http_port": value, + }, commonRepo.WithByID(appInstall.ID)) + } + + ComposeFile := fmt.Sprintf("%s/%s/%s/docker-compose.yml", constant.AppInstallDir, appKey, appInstall.Name) + stdout, err := compose.Down(ComposeFile) + if err != nil { + return errors.New(stdout) + } + stdout, err = compose.Up(ComposeFile) + if err != nil { + return errors.New(stdout) + } + return nil +} diff --git a/agent/app/service/app_utils.go b/agent/app/service/app_utils.go new file mode 100644 index 000000000..f28a1d82f --- /dev/null +++ b/agent/app/service/app_utils.go @@ -0,0 +1,1521 @@ +package service + +import ( + "context" + "encoding/base64" + "encoding/json" + "fmt" + "math" + "net/http" + "os" + "os/exec" + "path" + "reflect" + "regexp" + "strconv" + "strings" + "time" + + "github.com/docker/docker/api/types" + + httpUtil "github.com/1Panel-dev/1Panel/agent/utils/http" + "github.com/1Panel-dev/1Panel/agent/utils/xpack" + "github.com/docker/docker/api/types/container" + + "github.com/1Panel-dev/1Panel/agent/utils/cmd" + + "github.com/1Panel-dev/1Panel/agent/app/api/v1/helper" + "github.com/1Panel-dev/1Panel/agent/app/dto/request" + "github.com/1Panel-dev/1Panel/agent/i18n" + "github.com/subosito/gotenv" + "gopkg.in/yaml.v3" + + "github.com/1Panel-dev/1Panel/agent/app/repo" + "github.com/1Panel-dev/1Panel/agent/utils/env" + + "github.com/1Panel-dev/1Panel/agent/app/dto/response" + "github.com/1Panel-dev/1Panel/agent/buserr" + + "github.com/1Panel-dev/1Panel/agent/app/dto" + "github.com/1Panel-dev/1Panel/agent/app/model" + "github.com/1Panel-dev/1Panel/agent/constant" + "github.com/1Panel-dev/1Panel/agent/global" + "github.com/1Panel-dev/1Panel/agent/utils/common" + "github.com/1Panel-dev/1Panel/agent/utils/compose" + "github.com/1Panel-dev/1Panel/agent/utils/docker" + composeV2 "github.com/1Panel-dev/1Panel/agent/utils/docker" + "github.com/1Panel-dev/1Panel/agent/utils/files" + "github.com/pkg/errors" +) + +type DatabaseOp string + +var ( + Add DatabaseOp = "add" + Delete DatabaseOp = "delete" +) + +func checkPort(key string, params map[string]interface{}) (int, error) { + port, ok := params[key] + if ok { + portN := 0 + var err error + switch p := port.(type) { + case string: + portN, err = strconv.Atoi(p) + if err != nil { + return portN, nil + } + case float64: + portN = int(math.Ceil(p)) + case int: + portN = p + } + + oldInstalled, _ := appInstallRepo.ListBy(appInstallRepo.WithPort(portN)) + if len(oldInstalled) > 0 { + var apps []string + for _, install := range oldInstalled { + apps = append(apps, install.App.Name) + } + return portN, buserr.WithMap(constant.ErrPortInOtherApp, map[string]interface{}{"port": portN, "apps": apps}, nil) + } + if common.ScanPort(portN) { + return portN, buserr.WithDetail(constant.ErrPortInUsed, portN, nil) + } else { + return portN, nil + } + } + return 0, nil +} + +func checkPortExist(port int) error { + errMap := make(map[string]interface{}) + errMap["port"] = port + appInstall, _ := appInstallRepo.GetFirst(appInstallRepo.WithPort(port)) + if appInstall.ID > 0 { + errMap["type"] = i18n.GetMsgByKey("TYPE_APP") + errMap["name"] = appInstall.Name + return buserr.WithMap("ErrPortExist", errMap, nil) + } + runtime, _ := runtimeRepo.GetFirst(runtimeRepo.WithPort(port)) + if runtime != nil { + errMap["type"] = i18n.GetMsgByKey("TYPE_RUNTIME") + errMap["name"] = runtime.Name + return buserr.WithMap("ErrPortExist", errMap, nil) + } + domain, _ := websiteDomainRepo.GetFirst(websiteDomainRepo.WithPort(port)) + if domain.ID > 0 { + errMap["type"] = i18n.GetMsgByKey("TYPE_DOMAIN") + errMap["name"] = domain.Domain + return buserr.WithMap("ErrPortExist", errMap, nil) + } + if common.ScanPort(port) { + return buserr.WithDetail(constant.ErrPortInUsed, port, nil) + } + return nil +} + +var DatabaseKeys = map[string]uint{ + constant.AppMysql: 3306, + constant.AppMariaDB: 3306, + constant.AppPostgresql: 5432, + constant.AppPostgres: 5432, + constant.AppMongodb: 27017, + constant.AppRedis: 6379, + constant.AppMemcached: 11211, +} + +var ToolKeys = map[string]uint{ + "minio": 9001, +} + +func createLink(ctx context.Context, app model.App, appInstall *model.AppInstall, params map[string]interface{}) error { + var dbConfig dto.AppDatabase + if DatabaseKeys[app.Key] > 0 { + database := &model.Database{ + AppInstallID: appInstall.ID, + Name: appInstall.Name, + Type: app.Key, + Version: appInstall.Version, + From: "local", + Address: appInstall.ServiceName, + Port: DatabaseKeys[app.Key], + } + detail, err := appDetailRepo.GetFirst(commonRepo.WithByID(appInstall.AppDetailId)) + if err != nil { + return err + } + + formFields := &dto.AppForm{} + if err := json.Unmarshal([]byte(detail.Params), formFields); err != nil { + return err + } + for _, form := range formFields.FormFields { + if form.EnvKey == "PANEL_APP_PORT_HTTP" { + portFloat, ok := form.Default.(float64) + if ok { + database.Port = uint(int(portFloat)) + } + break + } + } + + switch app.Key { + case constant.AppMysql, constant.AppMariaDB, constant.AppPostgresql, constant.AppMongodb: + if password, ok := params["PANEL_DB_ROOT_PASSWORD"]; ok { + if password != "" { + database.Password = password.(string) + if app.Key == "mysql" || app.Key == "mariadb" { + database.Username = "root" + } + if rootUser, ok := params["PANEL_DB_ROOT_USER"]; ok { + database.Username = rootUser.(string) + } + authParam := dto.AuthParam{ + RootPassword: password.(string), + RootUser: database.Username, + } + authByte, err := json.Marshal(authParam) + if err != nil { + return err + } + appInstall.Param = string(authByte) + + } + } + case constant.AppRedis: + if password, ok := params["PANEL_REDIS_ROOT_PASSWORD"]; ok { + if password != "" { + authParam := dto.RedisAuthParam{ + RootPassword: password.(string), + } + authByte, err := json.Marshal(authParam) + if err != nil { + return err + } + appInstall.Param = string(authByte) + } + database.Password = password.(string) + } + } + if err := databaseRepo.Create(ctx, database); err != nil { + return err + } + } + if ToolKeys[app.Key] > 0 { + if app.Key == "minio" { + authParam := dto.MinioAuthParam{} + if password, ok := params["PANEL_MINIO_ROOT_PASSWORD"]; ok { + authParam.RootPassword = password.(string) + } + if rootUser, ok := params["PANEL_MINIO_ROOT_USER"]; ok { + authParam.RootUser = rootUser.(string) + } + authByte, err := json.Marshal(authParam) + if err != nil { + return err + } + appInstall.Param = string(authByte) + } + } + + if app.Type == "website" || app.Type == "tool" { + paramByte, err := json.Marshal(params) + if err != nil { + return err + } + if err = json.Unmarshal(paramByte, &dbConfig); err != nil { + return err + } + } + + if !reflect.DeepEqual(dbConfig, dto.AppDatabase{}) && dbConfig.ServiceName != "" { + hostName := params["PANEL_DB_HOST_NAME"] + if hostName == nil || hostName.(string) == "" { + return nil + } + database, _ := databaseRepo.Get(commonRepo.WithByName(hostName.(string))) + if database.ID == 0 { + return nil + } + var resourceId uint + if dbConfig.DbName != "" && dbConfig.DbUser != "" && dbConfig.Password != "" { + switch database.Type { + case constant.AppPostgresql, constant.AppPostgres: + iPostgresqlRepo := repo.NewIPostgresqlRepo() + oldPostgresqlDb, _ := iPostgresqlRepo.Get(commonRepo.WithByName(dbConfig.DbName), iPostgresqlRepo.WithByFrom(constant.ResourceLocal)) + resourceId = oldPostgresqlDb.ID + if oldPostgresqlDb.ID > 0 { + if oldPostgresqlDb.Username != dbConfig.DbUser || oldPostgresqlDb.Password != dbConfig.Password { + return buserr.New(constant.ErrDbUserNotValid) + } + } else { + var createPostgresql dto.PostgresqlDBCreate + createPostgresql.Name = dbConfig.DbName + createPostgresql.Username = dbConfig.DbUser + createPostgresql.Database = database.Name + createPostgresql.Format = "UTF8" + createPostgresql.Password = dbConfig.Password + createPostgresql.From = database.From + createPostgresql.SuperUser = true + pgdb, err := NewIPostgresqlService().Create(ctx, createPostgresql) + if err != nil { + return err + } + resourceId = pgdb.ID + } + case constant.AppMysql, constant.AppMariaDB: + iMysqlRepo := repo.NewIMysqlRepo() + oldMysqlDb, _ := iMysqlRepo.Get(commonRepo.WithByName(dbConfig.DbName), iMysqlRepo.WithByFrom(constant.ResourceLocal)) + resourceId = oldMysqlDb.ID + if oldMysqlDb.ID > 0 { + if oldMysqlDb.Username != dbConfig.DbUser || oldMysqlDb.Password != dbConfig.Password { + return buserr.New(constant.ErrDbUserNotValid) + } + } else { + var createMysql dto.MysqlDBCreate + createMysql.Name = dbConfig.DbName + createMysql.Username = dbConfig.DbUser + createMysql.Database = database.Name + createMysql.Format = "utf8mb4" + createMysql.Permission = "%" + createMysql.Password = dbConfig.Password + createMysql.From = database.From + mysqldb, err := NewIMysqlService().Create(ctx, createMysql) + if err != nil { + return err + } + resourceId = mysqldb.ID + } + } + + } + var installResource model.AppInstallResource + installResource.ResourceId = resourceId + installResource.AppInstallId = appInstall.ID + if database.AppInstallID > 0 { + installResource.LinkId = database.AppInstallID + } else { + installResource.LinkId = database.ID + } + installResource.Key = database.Type + installResource.From = database.From + if err := appInstallResourceRepo.Create(ctx, &installResource); err != nil { + return err + } + } + return nil +} + +func handleAppInstallErr(ctx context.Context, install *model.AppInstall) error { + op := files.NewFileOp() + appDir := install.GetPath() + dir, _ := os.Stat(appDir) + if dir != nil { + _, _ = compose.Down(install.GetComposePath()) + if err := op.DeleteDir(appDir); err != nil { + return err + } + } + if err := deleteLink(ctx, install, true, true, true); err != nil { + return err + } + return nil +} + +func deleteAppInstall(install model.AppInstall, deleteBackup bool, forceDelete bool, deleteDB bool) error { + op := files.NewFileOp() + appDir := install.GetPath() + dir, _ := os.Stat(appDir) + if dir != nil { + out, err := compose.Down(install.GetComposePath()) + if err != nil && !forceDelete { + return handleErr(install, err, out) + } + if err = runScript(&install, "uninstall"); err != nil { + _, _ = compose.Up(install.GetComposePath()) + return err + } + } + tx, ctx := helper.GetTxAndContext() + defer tx.Rollback() + if err := appInstallRepo.Delete(ctx, install); err != nil { + return err + } + if err := deleteLink(ctx, &install, deleteDB, forceDelete, deleteBackup); err != nil && !forceDelete { + return err + } + + if DatabaseKeys[install.App.Key] > 0 { + _ = databaseRepo.Delete(ctx, databaseRepo.WithAppInstallID(install.ID)) + } + + switch install.App.Key { + case constant.AppOpenresty: + websites, _ := websiteRepo.List() + for _, website := range websites { + if website.AppInstallID > 0 { + websiteAppInstall, _ := appInstallRepo.GetFirst(commonRepo.WithByID(website.AppInstallID)) + if websiteAppInstall.AppId > 0 { + websiteApp, _ := appRepo.GetFirst(commonRepo.WithByID(websiteAppInstall.AppId)) + if websiteApp.Type == constant.RuntimePHP { + go func() { + _, _ = compose.Down(websiteAppInstall.GetComposePath()) + _ = op.DeleteDir(websiteAppInstall.GetPath()) + }() + _ = appInstallRepo.Delete(ctx, websiteAppInstall) + } + } + } + } + _ = websiteRepo.DeleteAll(ctx) + _ = websiteDomainRepo.DeleteAll(ctx) + xpack.RemoveTamper("") + case constant.AppMysql, constant.AppMariaDB: + _ = mysqlRepo.Delete(ctx, mysqlRepo.WithByMysqlName(install.Name)) + case constant.AppPostgresql: + _ = postgresqlRepo.Delete(ctx, postgresqlRepo.WithByPostgresqlName(install.Name)) + } + + _ = backupRepo.DeleteRecord(ctx, commonRepo.WithByType("app"), commonRepo.WithByName(install.App.Key), backupRepo.WithByDetailName(install.Name)) + uploadDir := path.Join(global.CONF.System.BaseDir, fmt.Sprintf("1panel/uploads/app/%s/%s", install.App.Key, install.Name)) + if _, err := os.Stat(uploadDir); err == nil { + _ = os.RemoveAll(uploadDir) + } + if deleteBackup { + localDir, _ := loadLocalDir() + backupDir := path.Join(localDir, fmt.Sprintf("app/%s/%s", install.App.Key, install.Name)) + if _, err := os.Stat(backupDir); err == nil { + _ = os.RemoveAll(backupDir) + } + global.LOG.Infof("delete app %s-%s backups successful", install.App.Key, install.Name) + } + _ = op.DeleteDir(appDir) + tx.Commit() + return nil +} + +func deleteLink(ctx context.Context, install *model.AppInstall, deleteDB bool, forceDelete bool, deleteBackup bool) error { + resources, _ := appInstallResourceRepo.GetBy(appInstallResourceRepo.WithAppInstallId(install.ID)) + if len(resources) == 0 { + return nil + } + for _, re := range resources { + if deleteDB { + switch re.Key { + case constant.AppMysql, constant.AppMariaDB: + mysqlService := NewIMysqlService() + database, _ := mysqlRepo.Get(commonRepo.WithByID(re.ResourceId)) + if reflect.DeepEqual(database, model.DatabaseMysql{}) { + continue + } + if err := mysqlService.Delete(ctx, dto.MysqlDBDelete{ + ID: database.ID, + ForceDelete: forceDelete, + DeleteBackup: deleteBackup, + Type: re.Key, + Database: database.MysqlName, + }); err != nil && !forceDelete { + return err + } + case constant.AppPostgresql: + pgsqlService := NewIPostgresqlService() + database, _ := postgresqlRepo.Get(commonRepo.WithByID(re.ResourceId)) + if reflect.DeepEqual(database, model.DatabasePostgresql{}) { + continue + } + if err := pgsqlService.Delete(ctx, dto.PostgresqlDBDelete{ + ID: database.ID, + ForceDelete: forceDelete, + DeleteBackup: deleteBackup, + Type: re.Key, + Database: database.PostgresqlName, + }); err != nil { + return err + } + } + } + + } + return appInstallResourceRepo.DeleteBy(ctx, appInstallResourceRepo.WithAppInstallId(install.ID)) +} + +func getUpgradeCompose(install model.AppInstall, detail model.AppDetail) (string, error) { + if detail.DockerCompose == "" { + return "", nil + } + composeMap := make(map[string]interface{}) + if err := yaml.Unmarshal([]byte(detail.DockerCompose), &composeMap); err != nil { + return "", err + } + value, ok := composeMap["services"] + if !ok || value == nil { + return "", buserr.New(constant.ErrFileParse) + } + servicesMap := value.(map[string]interface{}) + if len(servicesMap) == 1 { + index := 0 + oldServiceName := "" + for k := range servicesMap { + oldServiceName = k + index++ + if index > 0 { + break + } + } + servicesMap[install.ServiceName] = servicesMap[oldServiceName] + if install.ServiceName != oldServiceName { + delete(servicesMap, oldServiceName) + } + } + envs := make(map[string]interface{}) + if err := json.Unmarshal([]byte(install.Env), &envs); err != nil { + return "", err + } + config := getAppCommonConfig(envs) + if config.ContainerName == "" { + config.ContainerName = install.ContainerName + envs[constant.ContainerName] = install.ContainerName + } + config.Advanced = true + if err := addDockerComposeCommonParam(composeMap, install.ServiceName, config, envs); err != nil { + return "", err + } + paramByte, err := json.Marshal(envs) + if err != nil { + return "", err + } + install.Env = string(paramByte) + composeByte, err := yaml.Marshal(composeMap) + if err != nil { + return "", err + } + return string(composeByte), nil +} + +func upgradeInstall(req request.AppInstallUpgrade) error { + install, err := appInstallRepo.GetFirst(commonRepo.WithByID(req.InstallID)) + if err != nil { + return err + } + detail, err := appDetailRepo.GetFirst(commonRepo.WithByID(req.DetailID)) + if err != nil { + return err + } + if install.Version == detail.Version { + return errors.New("two version is same") + } + install.Status = constant.Upgrading + + go func() { + var ( + upErr error + backupFile string + preErr error + ) + global.LOG.Infof(i18n.GetMsgWithName("UpgradeAppStart", install.Name, nil)) + if req.Backup { + backupRecord, err := NewIBackupService().AppBackup(dto.CommonBackup{Name: install.App.Key, DetailName: install.Name}) + if err == nil { + localDir, err := loadLocalDir() + if err == nil { + backupFile = path.Join(localDir, backupRecord.FileDir, backupRecord.FileName) + } else { + global.LOG.Errorf(i18n.GetMsgWithName("ErrAppBackup", install.Name, err)) + } + } else { + global.LOG.Errorf(i18n.GetMsgWithName("ErrAppBackup", install.Name, err)) + } + } + + defer func() { + if upErr != nil { + global.LOG.Infof(i18n.GetMsgWithName("ErrAppUpgrade", install.Name, upErr)) + if req.Backup { + global.LOG.Infof(i18n.GetMsgWithName("AppRecover", install.Name, nil)) + if err := NewIBackupService().AppRecover(dto.CommonRecover{Name: install.App.Key, DetailName: install.Name, Type: "app", Source: constant.ResourceLocal, File: backupFile}); err != nil { + global.LOG.Errorf("recover app [%s] [%s] failed %v", install.App.Key, install.Name, err) + } + } + existInstall, _ := appInstallRepo.GetFirst(commonRepo.WithByID(req.InstallID)) + if existInstall.ID > 0 { + existInstall.Status = constant.UpgradeErr + existInstall.Message = upErr.Error() + _ = appInstallRepo.Save(context.Background(), &existInstall) + } + } + if preErr != nil { + global.LOG.Infof(i18n.GetMsgWithName("ErrAppUpgrade", install.Name, preErr)) + existInstall, _ := appInstallRepo.GetFirst(commonRepo.WithByID(req.InstallID)) + if existInstall.ID > 0 { + existInstall.Status = constant.UpgradeErr + existInstall.Message = preErr.Error() + _ = appInstallRepo.Save(context.Background(), &existInstall) + } + } + }() + + fileOp := files.NewFileOp() + detailDir := path.Join(constant.ResourceDir, "apps", install.App.Resource, install.App.Key, detail.Version) + if install.App.Resource == constant.AppResourceRemote { + if preErr = downloadApp(install.App, detail, &install); preErr != nil { + return + } + if detail.DockerCompose == "" { + composeDetail, err := fileOp.GetContent(path.Join(detailDir, "docker-compose.yml")) + if err != nil { + preErr = err + return + } + detail.DockerCompose = string(composeDetail) + _ = appDetailRepo.Update(context.Background(), detail) + } + go func() { + _, _, _ = httpUtil.HandleGet(detail.DownloadCallBackUrl, http.MethodGet, constant.TimeOut5s) + }() + } + + if install.App.Resource == constant.AppResourceLocal { + detailDir = path.Join(constant.ResourceDir, "apps", "local", strings.TrimPrefix(install.App.Key, "local"), detail.Version) + } + + content, err := fileOp.GetContent(install.GetEnvPath()) + if err != nil { + preErr = err + return + } + if req.PullImage { + projectName := strings.ToLower(install.Name) + images, err := composeV2.GetDockerComposeImages(projectName, content, []byte(detail.DockerCompose)) + if err != nil { + preErr = err + return + } + for _, image := range images { + global.LOG.Infof(i18n.GetMsgWithName("PullImageStart", image, nil)) + if out, err := cmd.ExecWithTimeOut("docker pull "+image, 20*time.Minute); err != nil { + if out != "" { + err = errors.New(out) + } + preErr = buserr.WithNameAndErr("ErrDockerPullImage", "", err) + return + } else { + global.LOG.Infof(i18n.GetMsgByKey("PullImageSuccess")) + } + } + } + + command := exec.Command("/bin/bash", "-c", fmt.Sprintf("cp -rn %s/* %s || true", detailDir, install.GetPath())) + stdout, _ := command.CombinedOutput() + if stdout != nil { + global.LOG.Infof("upgrade app [%s] [%s] cp file log : %s ", install.App.Key, install.Name, string(stdout)) + } + sourceScripts := path.Join(detailDir, "scripts") + if fileOp.Stat(sourceScripts) { + dstScripts := path.Join(install.GetPath(), "scripts") + _ = fileOp.DeleteDir(dstScripts) + _ = fileOp.CreateDir(dstScripts, 0755) + scriptCmd := exec.Command("cp", "-rf", sourceScripts+"/.", dstScripts+"/") + _, _ = scriptCmd.CombinedOutput() + } + + var newCompose string + if req.DockerCompose == "" { + newCompose, upErr = getUpgradeCompose(install, detail) + if upErr != nil { + return + } + } else { + newCompose = req.DockerCompose + } + + install.DockerCompose = newCompose + install.Version = detail.Version + install.AppDetailId = req.DetailID + + if out, err := compose.Down(install.GetComposePath()); err != nil { + if out != "" { + upErr = errors.New(out) + return + } + upErr = err + return + } + envs := make(map[string]interface{}) + if upErr = json.Unmarshal([]byte(install.Env), &envs); upErr != nil { + return + } + envParams := make(map[string]string, len(envs)) + handleMap(envs, envParams) + if upErr = env.Write(envParams, install.GetEnvPath()); upErr != nil { + return + } + + if upErr = runScript(&install, "upgrade"); upErr != nil { + return + } + + if upErr = fileOp.WriteFile(install.GetComposePath(), strings.NewReader(install.DockerCompose), 0775); upErr != nil { + return + } + if out, err := compose.Up(install.GetComposePath()); err != nil { + if out != "" { + upErr = errors.New(out) + return + } + upErr = err + return + } + install.Status = constant.Running + _ = appInstallRepo.Save(context.Background(), &install) + global.LOG.Infof(i18n.GetMsgWithName("UpgradeAppSuccess", install.Name, nil)) + }() + + return appInstallRepo.Save(context.Background(), &install) +} + +func getContainerNames(install model.AppInstall) ([]string, error) { + envStr, err := coverEnvJsonToStr(install.Env) + if err != nil { + return nil, err + } + project, err := composeV2.GetComposeProject(install.Name, install.GetPath(), []byte(install.DockerCompose), []byte(envStr), true) + if err != nil { + return nil, err + } + containerMap := make(map[string]struct{}) + for _, service := range project.AllServices() { + if service.ContainerName == "${CONTAINER_NAME}" || service.ContainerName == "" { + continue + } + containerMap[service.ContainerName] = struct{}{} + } + var containerNames []string + for k := range containerMap { + containerNames = append(containerNames, k) + } + if len(containerNames) == 0 { + containerNames = append(containerNames, install.ContainerName) + } + return containerNames, nil +} + +func coverEnvJsonToStr(envJson string) (string, error) { + envMap := make(map[string]interface{}) + _ = json.Unmarshal([]byte(envJson), &envMap) + newEnvMap := make(map[string]string, len(envMap)) + handleMap(envMap, newEnvMap) + envStr, err := gotenv.Marshal(newEnvMap) + if err != nil { + return "", err + } + return envStr, nil +} + +func checkLimit(app model.App) error { + if app.Limit > 0 { + installs, err := appInstallRepo.ListBy(appInstallRepo.WithAppId(app.ID)) + if err != nil { + return err + } + if len(installs) >= app.Limit { + return buserr.New(constant.ErrAppLimit) + } + } + return nil +} + +func checkRequiredAndLimit(app model.App) error { + if err := checkLimit(app); err != nil { + return err + } + return nil +} + +func handleMap(params map[string]interface{}, envParams map[string]string) { + for k, v := range params { + switch t := v.(type) { + case string: + envParams[k] = t + case float64: + envParams[k] = strconv.FormatFloat(t, 'f', -1, 32) + case uint: + envParams[k] = strconv.Itoa(int(t)) + case int: + envParams[k] = strconv.Itoa(t) + case []interface{}: + strArray := make([]string, len(t)) + for i := range t { + strArray[i] = strings.ToLower(fmt.Sprintf("%v", t[i])) + } + envParams[k] = strings.Join(strArray, ",") + case map[string]interface{}: + handleMap(t, envParams) + } + } +} + +func downloadApp(app model.App, appDetail model.AppDetail, appInstall *model.AppInstall) (err error) { + if app.IsLocalApp() { + //本地应用,不去官网下载 + return nil + } + appResourceDir := path.Join(constant.AppResourceDir, app.Resource) + appDownloadDir := app.GetAppResourcePath() + appVersionDir := path.Join(appDownloadDir, appDetail.Version) + fileOp := files.NewFileOp() + if !appDetail.Update && fileOp.Stat(appVersionDir) { + return + } + if !fileOp.Stat(appDownloadDir) { + _ = fileOp.CreateDir(appDownloadDir, 0755) + } + if !fileOp.Stat(appVersionDir) { + _ = fileOp.CreateDir(appVersionDir, 0755) + } + global.LOG.Infof("download app[%s] from %s", app.Name, appDetail.DownloadUrl) + filePath := path.Join(appVersionDir, app.Key+"-"+appDetail.Version+".tar.gz") + + defer func() { + if err != nil { + if appInstall != nil { + appInstall.Status = constant.DownloadErr + appInstall.Message = err.Error() + } + } + }() + + if err = fileOp.DownloadFileWithProxy(appDetail.DownloadUrl, filePath); err != nil { + global.LOG.Errorf("download app[%s] error %v", app.Name, err) + return + } + if err = fileOp.Decompress(filePath, appResourceDir, files.SdkTarGz, ""); err != nil { + global.LOG.Errorf("decompress app[%s] error %v", app.Name, err) + return + } + _ = fileOp.DeleteFile(filePath) + appDetail.Update = false + _ = appDetailRepo.Update(context.Background(), appDetail) + return +} + +func copyData(app model.App, appDetail model.AppDetail, appInstall *model.AppInstall, req request.AppInstallCreate) (err error) { + fileOp := files.NewFileOp() + appResourceDir := path.Join(constant.AppResourceDir, app.Resource) + + if app.Resource == constant.AppResourceRemote { + err = downloadApp(app, appDetail, appInstall) + if err != nil { + return + } + go func() { + _, _, _ = httpUtil.HandleGet(appDetail.DownloadCallBackUrl, http.MethodGet, constant.TimeOut5s) + }() + } + appKey := app.Key + installAppDir := path.Join(constant.AppInstallDir, app.Key) + if app.Resource == constant.AppResourceLocal { + appResourceDir = constant.LocalAppResourceDir + appKey = strings.TrimPrefix(app.Key, "local") + installAppDir = path.Join(constant.LocalAppInstallDir, appKey) + } + resourceDir := path.Join(appResourceDir, appKey, appDetail.Version) + + if !fileOp.Stat(installAppDir) { + if err = fileOp.CreateDir(installAppDir, 0755); err != nil { + return + } + } + appDir := path.Join(installAppDir, req.Name) + if fileOp.Stat(appDir) { + if err = fileOp.DeleteDir(appDir); err != nil { + return + } + } + if err = fileOp.Copy(resourceDir, installAppDir); err != nil { + return + } + versionDir := path.Join(installAppDir, appDetail.Version) + if err = fileOp.Rename(versionDir, appDir); err != nil { + return + } + envPath := path.Join(appDir, ".env") + + envParams := make(map[string]string, len(req.Params)) + handleMap(req.Params, envParams) + if err = env.Write(envParams, envPath); err != nil { + return + } + if err := fileOp.WriteFile(appInstall.GetComposePath(), strings.NewReader(appInstall.DockerCompose), 0755); err != nil { + return err + } + return +} + +func runScript(appInstall *model.AppInstall, operate string) error { + workDir := appInstall.GetPath() + scriptPath := "" + switch operate { + case "init": + scriptPath = path.Join(workDir, "scripts", "init.sh") + case "upgrade": + scriptPath = path.Join(workDir, "scripts", "upgrade.sh") + case "uninstall": + scriptPath = path.Join(workDir, "scripts", "uninstall.sh") + } + if !files.NewFileOp().Stat(scriptPath) { + return nil + } + out, err := cmd.ExecScript(scriptPath, workDir) + if err != nil { + if out != "" { + errMsg := fmt.Sprintf("run script %s error %s", scriptPath, out) + global.LOG.Error(errMsg) + return errors.New(errMsg) + } + return err + } + return nil +} + +func checkContainerNameIsExist(containerName, appDir string) (bool, error) { + client, err := composeV2.NewDockerClient() + if err != nil { + return false, err + } + defer client.Close() + var options container.ListOptions + list, err := client.ContainerList(context.Background(), options) + if err != nil { + return false, err + } + for _, container := range list { + if containerName == container.Names[0][1:] { + if workDir, ok := container.Labels[composeWorkdirLabel]; ok { + if workDir != appDir { + return true, nil + } + } else { + return true, nil + } + } + + } + return false, nil +} + +func upApp(appInstall *model.AppInstall, pullImages bool) { + upProject := func(appInstall *model.AppInstall) (err error) { + 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()) + if err != nil { + if out != "" { + appInstall.Message = errMsg + out + } + return err + } + return + } + if err := upProject(appInstall); err != nil { + appInstall.Status = constant.UpErr + } else { + appInstall.Status = constant.Running + } + exist, _ := appInstallRepo.GetFirst(commonRepo.WithByID(appInstall.ID)) + if exist.ID > 0 { + containerNames, err := getContainerNames(*appInstall) + if err != nil { + return + } + if len(containerNames) > 0 { + appInstall.ContainerName = strings.Join(containerNames, ",") + } + _ = appInstallRepo.Save(context.Background(), appInstall) + } +} + +func rebuildApp(appInstall model.AppInstall) error { + appInstall.Status = constant.Rebuilding + _ = appInstallRepo.Save(context.Background(), &appInstall) + go func() { + dockerComposePath := appInstall.GetComposePath() + out, err := compose.Down(dockerComposePath) + if err != nil { + _ = handleErr(appInstall, err, out) + return + } + out, err = compose.Up(appInstall.GetComposePath()) + if err != nil { + _ = handleErr(appInstall, err, out) + return + } + containerNames, err := getContainerNames(appInstall) + if err != nil { + _ = handleErr(appInstall, err, out) + return + } + appInstall.ContainerName = strings.Join(containerNames, ",") + + appInstall.Status = constant.Running + _ = appInstallRepo.Save(context.Background(), &appInstall) + }() + return nil +} + +func getAppDetails(details []model.AppDetail, versions []dto.AppConfigVersion) map[string]model.AppDetail { + appDetails := make(map[string]model.AppDetail, len(details)) + for _, old := range details { + old.Status = constant.AppTakeDown + appDetails[old.Version] = old + } + for _, v := range versions { + version := v.Name + detail, ok := appDetails[version] + if ok { + detail.Status = constant.AppNormal + appDetails[version] = detail + } else { + appDetails[version] = model.AppDetail{ + Version: version, + Status: constant.AppNormal, + } + } + } + return appDetails +} + +func getApps(oldApps []model.App, items []dto.AppDefine) map[string]model.App { + apps := make(map[string]model.App, len(oldApps)) + for _, old := range oldApps { + old.Status = constant.AppTakeDown + apps[old.Key] = old + } + for _, item := range items { + config := item.AppProperty + key := config.Key + app, ok := apps[key] + if !ok { + app = model.App{} + } + app.Resource = constant.AppResourceRemote + app.Name = item.Name + app.Limit = config.Limit + app.Key = key + app.ShortDescZh = config.ShortDescZh + app.ShortDescEn = config.ShortDescEn + app.Website = config.Website + app.Document = config.Document + app.Github = config.Github + app.Type = config.Type + app.CrossVersionUpdate = config.CrossVersionUpdate + app.Status = constant.AppNormal + app.LastModified = item.LastModified + app.ReadMe = item.ReadMe + apps[key] = app + } + return apps +} + +func handleLocalAppDetail(versionDir string, appDetail *model.AppDetail) error { + fileOp := files.NewFileOp() + dockerComposePath := path.Join(versionDir, "docker-compose.yml") + if !fileOp.Stat(dockerComposePath) { + return buserr.WithName(constant.ErrFileNotFound, "docker-compose.yml") + } + dockerComposeByte, _ := fileOp.GetContent(dockerComposePath) + if dockerComposeByte == nil { + return buserr.WithName(constant.ErrFileParseApp, "docker-compose.yml") + } + appDetail.DockerCompose = string(dockerComposeByte) + paramPath := path.Join(versionDir, "data.yml") + if !fileOp.Stat(paramPath) { + return buserr.WithName(constant.ErrFileNotFound, "data.yml") + } + paramByte, _ := fileOp.GetContent(paramPath) + if paramByte == nil { + return buserr.WithName(constant.ErrFileNotFound, "data.yml") + } + appParamConfig := dto.LocalAppParam{} + if err := yaml.Unmarshal(paramByte, &appParamConfig); err != nil { + return buserr.WithMap(constant.ErrFileParseApp, map[string]interface{}{"name": "data.yml", "err": err.Error()}, err) + } + dataJson, err := json.Marshal(appParamConfig.AppParams) + 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 +} + +func handleLocalApp(appDir string) (app *model.App, err error) { + fileOp := files.NewFileOp() + configYamlPath := path.Join(appDir, "data.yml") + if !fileOp.Stat(configYamlPath) { + err = buserr.WithName(constant.ErrFileNotFound, "data.yml") + return + } + iconPath := path.Join(appDir, "logo.png") + if !fileOp.Stat(iconPath) { + err = buserr.WithName(constant.ErrFileNotFound, "logo.png") + return + } + configYamlByte, err := fileOp.GetContent(configYamlPath) + if err != nil { + err = buserr.WithMap(constant.ErrFileParseApp, map[string]interface{}{"name": "data.yml", "err": err.Error()}, err) + return + } + localAppDefine := dto.LocalAppAppDefine{} + if err = yaml.Unmarshal(configYamlByte, &localAppDefine); err != nil { + err = buserr.WithMap(constant.ErrFileParseApp, map[string]interface{}{"name": "data.yml", "err": err.Error()}, err) + return + } + app = &localAppDefine.AppProperty + app.Resource = constant.AppResourceLocal + app.Status = constant.AppNormal + app.Recommend = 9999 + app.TagsKey = append(app.TagsKey, "Local") + app.Key = "local" + app.Key + readMePath := path.Join(appDir, "README.md") + readMeByte, err := fileOp.GetContent(readMePath) + if err == nil { + app.ReadMe = string(readMeByte) + } + iconByte, _ := fileOp.GetContent(iconPath) + if iconByte != nil { + iconStr := base64.StdEncoding.EncodeToString(iconByte) + app.Icon = iconStr + } + return +} + +func handleErr(install model.AppInstall, err error, out string) error { + reErr := err + install.Message = err.Error() + if out != "" { + install.Message = out + reErr = errors.New(out) + } + install.Status = constant.UpErr + _ = appInstallRepo.Save(context.Background(), &install) + return reErr +} + +func doNotNeedSync(installed model.AppInstall) bool { + return installed.Status == constant.Installing || installed.Status == constant.Rebuilding || installed.Status == constant.Upgrading || + installed.Status == constant.Syncing +} + +func synAppInstall(containers map[string]types.Container, appInstall *model.AppInstall, force bool) { + containerNames := strings.Split(appInstall.ContainerName, ",") + if len(containers) == 0 { + if appInstall.Status == constant.UpErr && !force { + return + } + appInstall.Status = constant.Error + appInstall.Message = buserr.WithName("ErrContainerNotFound", strings.Join(containerNames, ",")).Error() + _ = appInstallRepo.Save(context.Background(), appInstall) + return + } + notFoundNames := make([]string, 0) + exitNames := make([]string, 0) + exitedCount := 0 + pausedCount := 0 + runningCount := 0 + total := len(containerNames) + for _, name := range containerNames { + if con, ok := containers["/"+name]; ok { + switch con.State { + case "exited": + exitedCount++ + exitNames = append(exitNames, name) + case "running": + runningCount++ + case "paused": + pausedCount++ + } + } else { + notFoundNames = append(notFoundNames, name) + } + } + switch { + case exitedCount == total: + appInstall.Status = constant.Stopped + case runningCount == total: + appInstall.Status = constant.Running + case pausedCount == total: + appInstall.Status = constant.Paused + case len(notFoundNames) == total: + if appInstall.Status == constant.UpErr && !force { + return + } + appInstall.Status = constant.Error + appInstall.Message = buserr.WithName("ErrContainerNotFound", strings.Join(notFoundNames, ",")).Error() + default: + var msg string + if exitedCount > 0 { + msg = buserr.WithName("ErrContainerMsg", strings.Join(exitNames, ",")).Error() + } + if len(notFoundNames) > 0 { + msg += buserr.WithName("ErrContainerNotFound", strings.Join(notFoundNames, ",")).Error() + } + if msg == "" { + msg = buserr.New("ErrAppWarn").Error() + } + appInstall.Message = msg + appInstall.Status = constant.UnHealthy + } + _ = appInstallRepo.Save(context.Background(), appInstall) +} + +func handleInstalled(appInstallList []model.AppInstall, updated bool, sync bool) ([]response.AppInstallDTO, error) { + var ( + res []response.AppInstallDTO + containersMap map[string]types.Container + ) + if sync { + cli, err := docker.NewClient() + if err != nil { + return nil, err + } + defer cli.Close() + containers, err := cli.ListAllContainers() + if err != nil { + return nil, err + } + containersMap = make(map[string]types.Container, len(containers)) + for _, contain := range containers { + containersMap[contain.Names[0]] = contain + } + } + + for _, installed := range appInstallList { + if updated && (installed.App.Type == "php" || installed.Status == constant.Installing || (installed.App.Key == constant.AppMysql && installed.Version == "5.6.51")) { + continue + } + if sync && !doNotNeedSync(installed) { + synAppInstall(containersMap, &installed, false) + } + + installDTO := response.AppInstallDTO{ + ID: installed.ID, + Name: installed.Name, + AppID: installed.AppId, + AppDetailID: installed.AppDetailId, + Version: installed.Version, + Status: installed.Status, + Message: installed.Message, + HttpPort: installed.HttpPort, + HttpsPort: installed.HttpsPort, + Icon: installed.App.Icon, + AppName: installed.App.Name, + AppKey: installed.App.Key, + AppType: installed.App.Type, + Path: installed.GetPath(), + CreatedAt: installed.CreatedAt, + App: response.AppDetail{ + Github: installed.App.Github, + Website: installed.App.Website, + Document: installed.App.Document, + }, + } + if updated { + installDTO.DockerCompose = installed.DockerCompose + } + app, err := appRepo.GetFirst(commonRepo.WithByID(installed.AppId)) + if err != nil { + return nil, err + } + details, err := appDetailRepo.GetBy(appDetailRepo.WithAppId(app.ID)) + if err != nil { + return nil, err + } + var versions []string + for _, detail := range details { + if detail.IgnoreUpgrade || installed.Version == "latest" { + continue + } + if common.IsCrossVersion(installed.Version, detail.Version) && !app.CrossVersionUpdate { + continue + } + versions = append(versions, detail.Version) + } + versions = common.GetSortedVersions(versions) + if len(versions) == 0 { + if !updated { + installDTO.CanUpdate = false + res = append(res, installDTO) + } + continue + } + lastVersion := versions[0] + if common.IsCrossVersion(installed.Version, lastVersion) { + installDTO.CanUpdate = app.CrossVersionUpdate + } else { + installDTO.CanUpdate = common.CompareVersion(lastVersion, installed.Version) + } + if updated { + if installDTO.CanUpdate { + res = append(res, installDTO) + } + } else { + res = append(res, installDTO) + } + } + return res, nil +} + +func getAppInstallByKey(key string) (model.AppInstall, error) { + app, err := appRepo.GetFirst(appRepo.WithKey(key)) + if err != nil { + return model.AppInstall{}, err + } + appInstall, err := appInstallRepo.GetFirst(appInstallRepo.WithAppId(app.ID)) + if err != nil { + return model.AppInstall{}, err + } + return appInstall, nil +} + +func getAppInstallPort(key string) (httpPort, httpsPort int, err error) { + install, err := getAppInstallByKey(key) + if err != nil { + return + } + httpPort = install.HttpPort + httpsPort = install.HttpsPort + return +} + +func updateToolApp(installed *model.AppInstall) { + tooKey, ok := dto.AppToolMap[installed.App.Key] + if !ok { + return + } + toolInstall, _ := getAppInstallByKey(tooKey) + if reflect.DeepEqual(toolInstall, model.AppInstall{}) { + return + } + paramMap := make(map[string]string) + _ = json.Unmarshal([]byte(installed.Param), ¶mMap) + envMap := make(map[string]interface{}) + _ = json.Unmarshal([]byte(toolInstall.Env), &envMap) + if password, ok := paramMap["PANEL_DB_ROOT_PASSWORD"]; ok { + envMap["PANEL_DB_ROOT_PASSWORD"] = password + } + if _, ok := envMap["PANEL_REDIS_HOST"]; ok { + envMap["PANEL_REDIS_HOST"] = installed.ServiceName + } + if _, ok := envMap["PANEL_DB_HOST"]; ok { + envMap["PANEL_DB_HOST"] = installed.ServiceName + } + + envPath := path.Join(toolInstall.GetPath(), ".env") + contentByte, err := json.Marshal(envMap) + if err != nil { + global.LOG.Errorf("update tool app [%s] error : %s", toolInstall.Name, err.Error()) + return + } + envFileMap := make(map[string]string) + handleMap(envMap, envFileMap) + if err = env.Write(envFileMap, envPath); err != nil { + global.LOG.Errorf("update tool app [%s] error : %s", toolInstall.Name, err.Error()) + return + } + toolInstall.Env = string(contentByte) + if err := appInstallRepo.Save(context.Background(), &toolInstall); err != nil { + global.LOG.Errorf("update tool app [%s] error : %s", toolInstall.Name, err.Error()) + return + } + if out, err := compose.Down(toolInstall.GetComposePath()); err != nil { + global.LOG.Errorf("update tool app [%s] error : %s", toolInstall.Name, out) + return + } + if out, err := compose.Up(toolInstall.GetComposePath()); err != nil { + global.LOG.Errorf("update tool app [%s] error : %s", toolInstall.Name, out) + return + } +} + +func addDockerComposeCommonParam(composeMap map[string]interface{}, serviceName string, req request.AppContainerConfig, params map[string]interface{}) error { + services, serviceValid := composeMap["services"].(map[string]interface{}) + if !serviceValid { + return buserr.New(constant.ErrFileParse) + } + service, serviceExist := services[serviceName] + if !serviceExist { + return buserr.New(constant.ErrFileParse) + } + serviceValue := service.(map[string]interface{}) + + deploy := map[string]interface{}{} + if de, ok := serviceValue["deploy"]; ok { + deploy = de.(map[string]interface{}) + } + resource := map[string]interface{}{} + if res, ok := deploy["resources"]; ok { + resource = res.(map[string]interface{}) + } + resource["limits"] = map[string]interface{}{ + "cpus": "${CPUS}", + "memory": "${MEMORY_LIMIT}", + } + deploy["resources"] = resource + serviceValue["deploy"] = deploy + + ports, ok := serviceValue["ports"].([]interface{}) + if ok { + for i, port := range ports { + portStr, portOK := port.(string) + if !portOK { + continue + } + portArray := strings.Split(portStr, ":") + if len(portArray) == 2 { + portArray = append([]string{"${HOST_IP}"}, portArray...) + } + ports[i] = strings.Join(portArray, ":") + } + serviceValue["ports"] = ports + } + + params[constant.CPUS] = "0" + params[constant.MemoryLimit] = "0" + if req.Advanced { + if req.CpuQuota > 0 { + params[constant.CPUS] = req.CpuQuota + } + if req.MemoryLimit > 0 { + params[constant.MemoryLimit] = strconv.FormatFloat(req.MemoryLimit, 'f', -1, 32) + req.MemoryUnit + } + } + _, portExist := serviceValue["ports"].([]interface{}) + if portExist { + allowHost := "127.0.0.1" + if req.Advanced && req.AllowPort { + allowHost = "" + } + params[constant.HostIP] = allowHost + } + services[serviceName] = serviceValue + return nil +} + +func getAppCommonConfig(envs map[string]interface{}) request.AppContainerConfig { + config := request.AppContainerConfig{} + + if hostIp, ok := envs[constant.HostIP]; ok { + config.AllowPort = hostIp.(string) != "127.0.0.1" + } else { + config.AllowPort = true + } + if cpuCore, ok := envs[constant.CPUS]; ok { + numStr, ok := cpuCore.(string) + if ok { + num, err := strconv.ParseFloat(numStr, 64) + if err == nil { + config.CpuQuota = num + } + } else { + num64, flOk := cpuCore.(float64) + if flOk { + config.CpuQuota = num64 + } + } + } else { + config.CpuQuota = 0 + } + if memLimit, ok := envs[constant.MemoryLimit]; ok { + re := regexp.MustCompile(`(\d+)([A-Za-z]+)`) + matches := re.FindStringSubmatch(memLimit.(string)) + if len(matches) == 3 { + num, err := strconv.ParseFloat(matches[1], 64) + if err == nil { + unit := matches[2] + config.MemoryLimit = num + config.MemoryUnit = unit + } + } + } else { + config.MemoryLimit = 0 + config.MemoryUnit = "M" + } + + if containerName, ok := envs[constant.ContainerName]; ok { + config.ContainerName = containerName.(string) + } + + return config +} + +func isHostModel(dockerCompose string) bool { + composeMap := make(map[string]interface{}) + _ = yaml.Unmarshal([]byte(dockerCompose), &composeMap) + services, serviceValid := composeMap["services"].(map[string]interface{}) + if !serviceValid { + return false + } + for _, service := range services { + serviceValue := service.(map[string]interface{}) + if value, ok := serviceValue["network_mode"]; ok && value == "host" { + return true + } + } + return false +} diff --git a/agent/app/service/backup.go b/agent/app/service/backup.go new file mode 100644 index 000000000..c7520c355 --- /dev/null +++ b/agent/app/service/backup.go @@ -0,0 +1,637 @@ +package service + +import ( + "bufio" + "context" + "encoding/base64" + "encoding/json" + "fmt" + "os" + "path" + "sort" + "strings" + "sync" + "time" + + "github.com/1Panel-dev/1Panel/agent/app/dto" + "github.com/1Panel-dev/1Panel/agent/app/model" + "github.com/1Panel-dev/1Panel/agent/buserr" + "github.com/1Panel-dev/1Panel/agent/constant" + "github.com/1Panel-dev/1Panel/agent/global" + "github.com/1Panel-dev/1Panel/agent/utils/cloud_storage" + "github.com/1Panel-dev/1Panel/agent/utils/cloud_storage/client" + fileUtils "github.com/1Panel-dev/1Panel/agent/utils/files" + "github.com/jinzhu/copier" + "github.com/pkg/errors" +) + +type BackupService struct{} + +type IBackupService interface { + List() ([]dto.BackupInfo, error) + SearchRecordsWithPage(search dto.RecordSearch) (int64, []dto.BackupRecords, error) + SearchRecordsByCronjobWithPage(search dto.RecordSearchByCronjob) (int64, []dto.BackupRecords, error) + LoadOneDriveInfo() (dto.OneDriveInfo, error) + DownloadRecord(info dto.DownloadRecord) (string, error) + Create(backupDto dto.BackupOperate) error + GetBuckets(backupDto dto.ForBuckets) ([]interface{}, error) + Update(ireq dto.BackupOperate) error + Delete(id uint) error + DeleteRecordByName(backupType, name, detailName string, withDeleteFile bool) error + BatchDeleteRecord(ids []uint) error + NewClient(backup *model.BackupAccount) (cloud_storage.CloudStorageClient, error) + + ListFiles(req dto.BackupSearchFile) []string + + MysqlBackup(db dto.CommonBackup) error + PostgresqlBackup(db dto.CommonBackup) error + MysqlRecover(db dto.CommonRecover) error + PostgresqlRecover(db dto.CommonRecover) error + MysqlRecoverByUpload(req dto.CommonRecover) error + PostgresqlRecoverByUpload(req dto.CommonRecover) error + + RedisBackup(db dto.CommonBackup) error + RedisRecover(db dto.CommonRecover) error + + WebsiteBackup(db dto.CommonBackup) error + WebsiteRecover(req dto.CommonRecover) error + + AppBackup(db dto.CommonBackup) (*model.BackupRecord, error) + AppRecover(req dto.CommonRecover) error + + Run() +} + +func NewIBackupService() IBackupService { + return &BackupService{} +} + +func (u *BackupService) List() ([]dto.BackupInfo, error) { + ops, err := backupRepo.List(commonRepo.WithOrderBy("created_at desc")) + var dtobas []dto.BackupInfo + dtobas = append(dtobas, u.loadByType("LOCAL", ops)) + dtobas = append(dtobas, u.loadByType("OSS", ops)) + dtobas = append(dtobas, u.loadByType("S3", ops)) + dtobas = append(dtobas, u.loadByType("SFTP", ops)) + dtobas = append(dtobas, u.loadByType("MINIO", ops)) + dtobas = append(dtobas, u.loadByType("COS", ops)) + dtobas = append(dtobas, u.loadByType("KODO", ops)) + dtobas = append(dtobas, u.loadByType("OneDrive", ops)) + dtobas = append(dtobas, u.loadByType("WebDAV", ops)) + return dtobas, err +} + +func (u *BackupService) SearchRecordsWithPage(search dto.RecordSearch) (int64, []dto.BackupRecords, error) { + total, records, err := backupRepo.PageRecord( + search.Page, search.PageSize, + commonRepo.WithOrderBy("created_at desc"), + commonRepo.WithByName(search.Name), + commonRepo.WithByType(search.Type), + backupRepo.WithByDetailName(search.DetailName), + ) + if err != nil { + return 0, nil, err + } + + datas, err := u.loadRecordSize(records) + sort.Slice(datas, func(i, j int) bool { + return datas[i].CreatedAt.After(datas[j].CreatedAt) + }) + return total, datas, err +} + +func (u *BackupService) SearchRecordsByCronjobWithPage(search dto.RecordSearchByCronjob) (int64, []dto.BackupRecords, error) { + total, records, err := backupRepo.PageRecord( + search.Page, search.PageSize, + commonRepo.WithOrderBy("created_at desc"), + backupRepo.WithByCronID(search.CronjobID), + ) + if err != nil { + return 0, nil, err + } + + datas, err := u.loadRecordSize(records) + sort.Slice(datas, func(i, j int) bool { + return datas[i].CreatedAt.After(datas[j].CreatedAt) + }) + return total, datas, err +} + +type loadSizeHelper struct { + isOk bool + backupPath string + client cloud_storage.CloudStorageClient +} + +func (u *BackupService) LoadOneDriveInfo() (dto.OneDriveInfo, error) { + var data dto.OneDriveInfo + data.RedirectUri = constant.OneDriveRedirectURI + clientID, err := settingRepo.Get(settingRepo.WithByKey("OneDriveID")) + if err != nil { + return data, err + } + idItem, err := base64.StdEncoding.DecodeString(clientID.Value) + if err != nil { + return data, err + } + data.ClientID = string(idItem) + clientSecret, err := settingRepo.Get(settingRepo.WithByKey("OneDriveSc")) + if err != nil { + return data, err + } + secretItem, err := base64.StdEncoding.DecodeString(clientSecret.Value) + if err != nil { + return data, err + } + data.ClientSecret = string(secretItem) + + return data, err +} + +func (u *BackupService) DownloadRecord(info dto.DownloadRecord) (string, error) { + if info.Source == "LOCAL" { + localDir, err := loadLocalDir() + if err != nil { + return "", err + } + return path.Join(localDir, info.FileDir, info.FileName), nil + } + backup, _ := backupRepo.Get(commonRepo.WithByType(info.Source)) + if backup.ID == 0 { + return "", constant.ErrRecordNotFound + } + varMap := make(map[string]interface{}) + if err := json.Unmarshal([]byte(backup.Vars), &varMap); err != nil { + return "", err + } + varMap["bucket"] = backup.Bucket + switch backup.Type { + case constant.Sftp, constant.WebDAV: + varMap["username"] = backup.AccessKey + varMap["password"] = backup.Credential + case constant.OSS, constant.S3, constant.MinIo, constant.Cos, constant.Kodo: + varMap["accessKey"] = backup.AccessKey + varMap["secretKey"] = backup.Credential + case constant.OneDrive: + varMap["accessToken"] = backup.Credential + } + backClient, err := cloud_storage.NewCloudStorageClient(backup.Type, varMap) + if err != nil { + return "", fmt.Errorf("new cloud storage client failed, err: %v", err) + } + targetPath := fmt.Sprintf("%s/download/%s/%s", constant.DataDir, info.FileDir, info.FileName) + if _, err := os.Stat(path.Dir(targetPath)); err != nil && os.IsNotExist(err) { + if err = os.MkdirAll(path.Dir(targetPath), os.ModePerm); err != nil { + global.LOG.Errorf("mkdir %s failed, err: %v", path.Dir(targetPath), err) + } + } + srcPath := fmt.Sprintf("%s/%s", info.FileDir, info.FileName) + if len(backup.BackupPath) != 0 { + srcPath = path.Join(strings.TrimPrefix(backup.BackupPath, "/"), srcPath) + } + if exist, _ := backClient.Exist(srcPath); exist { + isOK, err := backClient.Download(srcPath, targetPath) + if !isOK { + return "", fmt.Errorf("cloud storage download failed, err: %v", err) + } + } + return targetPath, nil +} + +func (u *BackupService) Create(req dto.BackupOperate) error { + backup, _ := backupRepo.Get(commonRepo.WithByType(req.Type)) + if backup.ID != 0 { + return constant.ErrRecordExist + } + if err := copier.Copy(&backup, &req); err != nil { + return errors.WithMessage(constant.ErrStructTransform, err.Error()) + } + + if req.Type == constant.OneDrive { + if err := u.loadAccessToken(&backup); err != nil { + return err + } + } + if req.Type != "LOCAL" { + if _, err := u.checkBackupConn(&backup); err != nil { + return buserr.WithMap("ErrBackupCheck", map[string]interface{}{"err": err.Error()}, err) + } + } + if backup.Type == constant.OneDrive { + StartRefreshOneDriveToken() + } + if err := backupRepo.Create(&backup); err != nil { + return err + } + return nil +} + +func (u *BackupService) GetBuckets(backupDto dto.ForBuckets) ([]interface{}, error) { + varMap := make(map[string]interface{}) + if err := json.Unmarshal([]byte(backupDto.Vars), &varMap); err != nil { + return nil, err + } + switch backupDto.Type { + case constant.Sftp, constant.WebDAV: + varMap["username"] = backupDto.AccessKey + varMap["password"] = backupDto.Credential + case constant.OSS, constant.S3, constant.MinIo, constant.Cos, constant.Kodo: + varMap["accessKey"] = backupDto.AccessKey + varMap["secretKey"] = backupDto.Credential + } + client, err := cloud_storage.NewCloudStorageClient(backupDto.Type, varMap) + if err != nil { + return nil, err + } + return client.ListBuckets() +} + +func (u *BackupService) Delete(id uint) error { + backup, _ := backupRepo.Get(commonRepo.WithByID(id)) + if backup.ID == 0 { + return constant.ErrRecordNotFound + } + if backup.Type == constant.OneDrive { + global.Cron.Remove(global.OneDriveCronID) + } + cronjobs, _ := cronjobRepo.List(cronjobRepo.WithByDefaultDownload(backup.Type)) + if len(cronjobs) != 0 { + return buserr.New(constant.ErrBackupInUsed) + } + return backupRepo.Delete(commonRepo.WithByID(id)) +} + +func (u *BackupService) DeleteRecordByName(backupType, name, detailName string, withDeleteFile bool) error { + if !withDeleteFile { + return backupRepo.DeleteRecord(context.Background(), commonRepo.WithByType(backupType), commonRepo.WithByName(name), backupRepo.WithByDetailName(detailName)) + } + + records, err := backupRepo.ListRecord(commonRepo.WithByType(backupType), commonRepo.WithByName(name), backupRepo.WithByDetailName(detailName)) + if err != nil { + return err + } + + for _, record := range records { + backupAccount, err := backupRepo.Get(commonRepo.WithByType(record.Source)) + if err != nil { + global.LOG.Errorf("load backup account %s info from db failed, err: %v", record.Source, err) + continue + } + client, err := u.NewClient(&backupAccount) + if err != nil { + global.LOG.Errorf("new client for backup account %s failed, err: %v", record.Source, err) + continue + } + if _, err = client.Delete(path.Join(record.FileDir, record.FileName)); err != nil { + global.LOG.Errorf("remove file %s from %s failed, err: %v", path.Join(record.FileDir, record.FileName), record.Source, err) + } + _ = backupRepo.DeleteRecord(context.Background(), commonRepo.WithByID(record.ID)) + } + return nil +} + +func (u *BackupService) BatchDeleteRecord(ids []uint) error { + records, err := backupRepo.ListRecord(commonRepo.WithIdsIn(ids)) + if err != nil { + return err + } + for _, record := range records { + backupAccount, err := backupRepo.Get(commonRepo.WithByType(record.Source)) + if err != nil { + global.LOG.Errorf("load backup account %s info from db failed, err: %v", record.Source, err) + continue + } + client, err := u.NewClient(&backupAccount) + if err != nil { + global.LOG.Errorf("new client for backup account %s failed, err: %v", record.Source, err) + continue + } + if _, err = client.Delete(path.Join(record.FileDir, record.FileName)); err != nil { + global.LOG.Errorf("remove file %s from %s failed, err: %v", path.Join(record.FileDir, record.FileName), record.Source, err) + } + } + return backupRepo.DeleteRecord(context.Background(), commonRepo.WithIdsIn(ids)) +} + +func (u *BackupService) Update(req dto.BackupOperate) error { + backup, err := backupRepo.Get(commonRepo.WithByID(req.ID)) + if err != nil { + return constant.ErrRecordNotFound + } + varMap := make(map[string]interface{}) + if err := json.Unmarshal([]byte(req.Vars), &varMap); err != nil { + return err + } + + oldVars := backup.Vars + oldDir, err := loadLocalDir() + if err != nil { + return err + } + upMap := make(map[string]interface{}) + upMap["bucket"] = req.Bucket + upMap["access_key"] = req.AccessKey + upMap["credential"] = req.Credential + upMap["backup_path"] = req.BackupPath + upMap["vars"] = req.Vars + backup.Bucket = req.Bucket + backup.Vars = req.Vars + backup.Credential = req.Credential + backup.AccessKey = req.AccessKey + backup.BackupPath = req.BackupPath + + if req.Type == constant.OneDrive { + if err := u.loadAccessToken(&backup); err != nil { + return err + } + upMap["credential"] = backup.Credential + upMap["vars"] = backup.Vars + } + if backup.Type != "LOCAL" { + isOk, err := u.checkBackupConn(&backup) + if err != nil || !isOk { + return buserr.WithMap("ErrBackupCheck", map[string]interface{}{"err": err.Error()}, err) + } + } + + if err := backupRepo.Update(req.ID, upMap); err != nil { + return err + } + if backup.Type == "LOCAL" { + if dir, ok := varMap["dir"]; ok { + if dirStr, isStr := dir.(string); isStr { + if strings.HasSuffix(dirStr, "/") && dirStr != "/" { + dirStr = dirStr[:strings.LastIndex(dirStr, "/")] + } + if err := copyDir(oldDir, dirStr); err != nil { + _ = backupRepo.Update(req.ID, map[string]interface{}{"vars": oldVars}) + return err + } + global.CONF.System.Backup = dirStr + } + } + } + return nil +} + +func (u *BackupService) ListFiles(req dto.BackupSearchFile) []string { + var datas []string + backup, err := backupRepo.Get(backupRepo.WithByType(req.Type)) + if err != nil { + return datas + } + client, err := u.NewClient(&backup) + if err != nil { + return datas + } + prefix := "system_snapshot" + if len(backup.BackupPath) != 0 { + prefix = path.Join(strings.TrimPrefix(backup.BackupPath, "/"), prefix) + } + files, err := client.ListObjects(prefix) + if err != nil { + global.LOG.Debugf("load files from %s failed, err: %v", req.Type, err) + return datas + } + for _, file := range files { + if len(file) != 0 { + datas = append(datas, path.Base(file)) + } + } + return datas +} + +func (u *BackupService) NewClient(backup *model.BackupAccount) (cloud_storage.CloudStorageClient, error) { + varMap := make(map[string]interface{}) + if err := json.Unmarshal([]byte(backup.Vars), &varMap); err != nil { + return nil, err + } + varMap["bucket"] = backup.Bucket + switch backup.Type { + case constant.Sftp, constant.WebDAV: + varMap["username"] = backup.AccessKey + varMap["password"] = backup.Credential + case constant.OSS, constant.S3, constant.MinIo, constant.Cos, constant.Kodo: + varMap["accessKey"] = backup.AccessKey + varMap["secretKey"] = backup.Credential + } + + backClient, err := cloud_storage.NewCloudStorageClient(backup.Type, varMap) + if err != nil { + return nil, err + } + + return backClient, nil +} + +func (u *BackupService) loadByType(accountType string, accounts []model.BackupAccount) dto.BackupInfo { + for _, account := range accounts { + if account.Type == accountType { + var item dto.BackupInfo + if err := copier.Copy(&item, &account); err != nil { + global.LOG.Errorf("copy backup account to dto backup info failed, err: %v", err) + } + if account.Type == constant.OneDrive { + varMap := make(map[string]interface{}) + if err := json.Unmarshal([]byte(item.Vars), &varMap); err != nil { + return dto.BackupInfo{Type: accountType} + } + delete(varMap, "refresh_token") + itemVars, _ := json.Marshal(varMap) + item.Vars = string(itemVars) + } + return item + } + } + return dto.BackupInfo{Type: accountType} +} + +func (u *BackupService) loadAccessToken(backup *model.BackupAccount) error { + varMap := make(map[string]interface{}) + if err := json.Unmarshal([]byte(backup.Vars), &varMap); err != nil { + return fmt.Errorf("unmarshal backup vars failed, err: %v", err) + } + refreshToken, err := client.RefreshToken("authorization_code", "refreshToken", varMap) + if err != nil { + return err + } + delete(varMap, "code") + varMap["refresh_status"] = constant.StatusSuccess + varMap["refresh_time"] = time.Now().Format(constant.DateTimeLayout) + varMap["refresh_token"] = refreshToken + itemVars, err := json.Marshal(varMap) + if err != nil { + return fmt.Errorf("json marshal var map failed, err: %v", err) + } + backup.Vars = string(itemVars) + return nil +} + +func (u *BackupService) loadRecordSize(records []model.BackupRecord) ([]dto.BackupRecords, error) { + var datas []dto.BackupRecords + clientMap := make(map[string]loadSizeHelper) + var wg sync.WaitGroup + for i := 0; i < len(records); i++ { + var item dto.BackupRecords + if err := copier.Copy(&item, &records[i]); err != nil { + return nil, errors.WithMessage(constant.ErrStructTransform, err.Error()) + } + itemPath := path.Join(records[i].FileDir, records[i].FileName) + if _, ok := clientMap[records[i].Source]; !ok { + backup, err := backupRepo.Get(commonRepo.WithByType(records[i].Source)) + if err != nil { + global.LOG.Errorf("load backup model %s from db failed, err: %v", records[i].Source, err) + clientMap[records[i].Source] = loadSizeHelper{} + datas = append(datas, item) + continue + } + client, err := u.NewClient(&backup) + if err != nil { + global.LOG.Errorf("load backup client %s from db failed, err: %v", records[i].Source, err) + clientMap[records[i].Source] = loadSizeHelper{} + datas = append(datas, item) + continue + } + item.Size, _ = client.Size(path.Join(strings.TrimLeft(backup.BackupPath, "/"), itemPath)) + datas = append(datas, item) + clientMap[records[i].Source] = loadSizeHelper{backupPath: strings.TrimLeft(backup.BackupPath, "/"), client: client, isOk: true} + continue + } + if clientMap[records[i].Source].isOk { + wg.Add(1) + go func(index int) { + item.Size, _ = clientMap[records[index].Source].client.Size(path.Join(clientMap[records[index].Source].backupPath, itemPath)) + datas = append(datas, item) + wg.Done() + }(i) + } else { + datas = append(datas, item) + } + } + wg.Wait() + return datas, nil +} + +func loadLocalDir() (string, error) { + backup, err := backupRepo.Get(commonRepo.WithByType("LOCAL")) + if err != nil { + return "", err + } + varMap := make(map[string]interface{}) + if err := json.Unmarshal([]byte(backup.Vars), &varMap); err != nil { + return "", err + } + if _, ok := varMap["dir"]; !ok { + return "", errors.New("load local backup dir failed") + } + baseDir, ok := varMap["dir"].(string) + if ok { + if _, err := os.Stat(baseDir); err != nil && os.IsNotExist(err) { + if err = os.MkdirAll(baseDir, os.ModePerm); err != nil { + return "", fmt.Errorf("mkdir %s failed, err: %v", baseDir, err) + } + } + return baseDir, nil + } + return "", fmt.Errorf("error type dir: %T", varMap["dir"]) +} + +func copyDir(src, dst string) error { + srcInfo, err := os.Stat(src) + if err != nil { + return err + } + if err = os.MkdirAll(dst, srcInfo.Mode()); err != nil { + return err + } + files, err := os.ReadDir(src) + if err != nil { + return err + } + + fileOP := fileUtils.NewFileOp() + for _, file := range files { + srcPath := fmt.Sprintf("%s/%s", src, file.Name()) + dstPath := fmt.Sprintf("%s/%s", dst, file.Name()) + if file.IsDir() { + if err = copyDir(srcPath, dstPath); err != nil { + global.LOG.Errorf("copy dir %s to %s failed, err: %v", srcPath, dstPath, err) + } + } else { + if err := fileOP.CopyFile(srcPath, dst); err != nil { + global.LOG.Errorf("copy file %s to %s failed, err: %v", srcPath, dstPath, err) + } + } + } + + return nil +} + +func (u *BackupService) checkBackupConn(backup *model.BackupAccount) (bool, error) { + client, err := u.NewClient(backup) + if err != nil { + return false, err + } + fileItem := path.Join(global.CONF.System.TmpDir, "test", "1panel") + if _, err := os.Stat(path.Dir(fileItem)); err != nil && os.IsNotExist(err) { + if err = os.MkdirAll(path.Dir(fileItem), os.ModePerm); err != nil { + return false, err + } + } + file, err := os.OpenFile(fileItem, os.O_WRONLY|os.O_CREATE, 0666) + if err != nil { + return false, err + } + defer file.Close() + write := bufio.NewWriter(file) + _, _ = write.WriteString("1Panel 备份账号测试文件。\n") + _, _ = write.WriteString("1Panel 備份賬號測試文件。\n") + _, _ = write.WriteString("1Panel Backs up account test files.\n") + _, _ = write.WriteString("1Panelアカウントのテストファイルをバックアップします。\n") + write.Flush() + + targetPath := strings.TrimPrefix(path.Join(backup.BackupPath, "test/1panel"), "/") + return client.Upload(fileItem, targetPath) +} + +func StartRefreshOneDriveToken() { + service := NewIBackupService() + oneDriveCronID, err := global.Cron.AddJob("0 3 */31 * *", service) + if err != nil { + global.LOG.Errorf("can not add OneDrive corn job: %s", err.Error()) + return + } + global.OneDriveCronID = oneDriveCronID +} + +func (u *BackupService) Run() { + var backupItem model.BackupAccount + _ = global.DB.Where("`type` = ?", "OneDrive").First(&backupItem) + if backupItem.ID == 0 { + return + } + global.LOG.Info("start to refresh token of OneDrive ...") + varMap := make(map[string]interface{}) + if err := json.Unmarshal([]byte(backupItem.Vars), &varMap); err != nil { + global.LOG.Errorf("Failed to refresh OneDrive token, please retry, err: %v", err) + return + } + refreshToken, err := client.RefreshToken("refresh_token", "refreshToken", varMap) + varMap["refresh_status"] = constant.StatusSuccess + varMap["refresh_time"] = time.Now().Format(constant.DateTimeLayout) + if err != nil { + varMap["refresh_status"] = constant.StatusFailed + varMap["refresh_msg"] = err.Error() + global.LOG.Errorf("Failed to refresh OneDrive token, please retry, err: %v", err) + return + } + varMap["refresh_token"] = refreshToken + + varsItem, _ := json.Marshal(varMap) + _ = global.DB.Model(&model.BackupAccount{}). + Where("id = ?", backupItem.ID). + Updates(map[string]interface{}{ + "vars": varsItem, + }).Error + global.LOG.Info("Successfully refreshed OneDrive token.") +} diff --git a/agent/app/service/backup_app.go b/agent/app/service/backup_app.go new file mode 100644 index 000000000..362712ee2 --- /dev/null +++ b/agent/app/service/backup_app.go @@ -0,0 +1,315 @@ +package service + +import ( + "context" + "encoding/json" + "fmt" + "io/fs" + "os" + "path" + "strings" + "time" + + "github.com/1Panel-dev/1Panel/agent/buserr" + + "github.com/1Panel-dev/1Panel/agent/app/dto" + "github.com/1Panel-dev/1Panel/agent/app/model" + "github.com/1Panel-dev/1Panel/agent/constant" + "github.com/1Panel-dev/1Panel/agent/global" + "github.com/1Panel-dev/1Panel/agent/utils/common" + "github.com/1Panel-dev/1Panel/agent/utils/compose" + "github.com/1Panel-dev/1Panel/agent/utils/files" + "github.com/pkg/errors" +) + +func (u *BackupService) AppBackup(req dto.CommonBackup) (*model.BackupRecord, error) { + localDir, err := loadLocalDir() + if err != nil { + return nil, err + } + app, err := appRepo.GetFirst(appRepo.WithKey(req.Name)) + if err != nil { + return nil, err + } + install, err := appInstallRepo.GetFirst(commonRepo.WithByName(req.DetailName), appInstallRepo.WithAppId(app.ID)) + if err != nil { + return nil, err + } + timeNow := time.Now().Format(constant.DateTimeSlimLayout) + itemDir := fmt.Sprintf("app/%s/%s", req.Name, req.DetailName) + backupDir := path.Join(localDir, itemDir) + + fileName := fmt.Sprintf("%s_%s.tar.gz", req.DetailName, timeNow+common.RandStrAndNum(5)) + if err := handleAppBackup(&install, backupDir, fileName, "", req.Secret); err != nil { + return nil, err + } + + record := &model.BackupRecord{ + Type: "app", + Name: req.Name, + DetailName: req.DetailName, + Source: "LOCAL", + BackupType: "LOCAL", + FileDir: itemDir, + FileName: fileName, + } + + if err := backupRepo.CreateRecord(record); err != nil { + global.LOG.Errorf("save backup record failed, err: %v", err) + return nil, err + } + return record, nil +} + +func (u *BackupService) AppRecover(req dto.CommonRecover) error { + app, err := appRepo.GetFirst(appRepo.WithKey(req.Name)) + if err != nil { + return err + } + install, err := appInstallRepo.GetFirst(commonRepo.WithByName(req.DetailName), appInstallRepo.WithAppId(app.ID)) + if err != nil { + return err + } + + fileOp := files.NewFileOp() + if !fileOp.Stat(req.File) { + return buserr.WithName("ErrFileNotFound", req.File) + } + if _, err := compose.Down(install.GetComposePath()); err != nil { + return err + } + if err := handleAppRecover(&install, req.File, false, req.Secret); err != nil { + return err + } + return nil +} + +func handleAppBackup(install *model.AppInstall, backupDir, fileName string, excludes string, secret string) error { + fileOp := files.NewFileOp() + tmpDir := fmt.Sprintf("%s/%s", backupDir, strings.ReplaceAll(fileName, ".tar.gz", "")) + if !fileOp.Stat(tmpDir) { + if err := os.MkdirAll(tmpDir, os.ModePerm); err != nil { + return fmt.Errorf("mkdir %s failed, err: %v", backupDir, err) + } + } + defer func() { + _ = os.RemoveAll(tmpDir) + }() + + remarkInfo, _ := json.Marshal(install) + remarkInfoPath := fmt.Sprintf("%s/app.json", tmpDir) + if err := fileOp.SaveFile(remarkInfoPath, string(remarkInfo), fs.ModePerm); err != nil { + return err + } + + appPath := install.GetPath() + if err := handleTar(appPath, tmpDir, "app.tar.gz", excludes, ""); err != nil { + return err + } + + resources, _ := appInstallResourceRepo.GetBy(appInstallResourceRepo.WithAppInstallId(install.ID)) + for _, resource := range resources { + switch resource.Key { + case constant.AppMysql, constant.AppMariaDB: + db, err := mysqlRepo.Get(commonRepo.WithByID(resource.ResourceId)) + if err != nil { + return err + } + if err := handleMysqlBackup(db.MysqlName, resource.Key, db.Name, tmpDir, fmt.Sprintf("%s.sql.gz", install.Name)); err != nil { + return err + } + case constant.AppPostgresql: + db, err := postgresqlRepo.Get(commonRepo.WithByID(resource.ResourceId)) + if err != nil { + return err + } + if err := handlePostgresqlBackup(db.PostgresqlName, db.Name, tmpDir, fmt.Sprintf("%s.sql.gz", install.Name)); err != nil { + return err + } + } + } + + if err := handleTar(tmpDir, backupDir, fileName, "", secret); err != nil { + return err + } + return nil +} + +func handleAppRecover(install *model.AppInstall, recoverFile string, isRollback bool, secret string) error { + isOk := false + fileOp := files.NewFileOp() + if err := handleUnTar(recoverFile, path.Dir(recoverFile), secret); err != nil { + return err + } + tmpPath := strings.ReplaceAll(recoverFile, ".tar.gz", "") + defer func() { + _, _ = compose.Up(install.GetComposePath()) + _ = os.RemoveAll(strings.ReplaceAll(recoverFile, ".tar.gz", "")) + }() + + if !fileOp.Stat(tmpPath+"/app.json") || !fileOp.Stat(tmpPath+"/app.tar.gz") { + return errors.New("the wrong recovery package does not have app.json or app.tar.gz files") + } + var oldInstall model.AppInstall + appjson, err := os.ReadFile(tmpPath + "/app.json") + if err != nil { + return err + } + if err := json.Unmarshal(appjson, &oldInstall); err != nil { + return fmt.Errorf("unmarshal app.json failed, err: %v", err) + } + if oldInstall.App.Key != install.App.Key || oldInstall.Name != install.Name { + return errors.New("the current backup file does not match the application") + } + + if !isRollback { + rollbackFile := path.Join(global.CONF.System.TmpDir, fmt.Sprintf("app/%s_%s.tar.gz", install.Name, time.Now().Format(constant.DateTimeSlimLayout))) + if err := handleAppBackup(install, path.Dir(rollbackFile), path.Base(rollbackFile), "", ""); err != nil { + return fmt.Errorf("backup app %s for rollback before recover failed, err: %v", install.Name, err) + } + defer func() { + if !isOk { + global.LOG.Info("recover failed, start to rollback now") + if err := handleAppRecover(install, rollbackFile, true, secret); err != nil { + global.LOG.Errorf("rollback app %s from %s failed, err: %v", install.Name, rollbackFile, err) + return + } + global.LOG.Infof("rollback app %s from %s successful", install.Name, rollbackFile) + _ = os.RemoveAll(rollbackFile) + } else { + _ = os.RemoveAll(rollbackFile) + } + }() + } + + newEnvFile := "" + resources, _ := appInstallResourceRepo.GetBy(appInstallResourceRepo.WithAppInstallId(install.ID)) + for _, resource := range resources { + var database model.Database + switch resource.From { + case constant.AppResourceRemote: + database, err = databaseRepo.Get(commonRepo.WithByID(resource.LinkId)) + if err != nil { + return err + } + case constant.AppResourceLocal: + resourceApp, err := appInstallRepo.GetFirst(commonRepo.WithByID(resource.LinkId)) + if err != nil { + return err + } + database, err = databaseRepo.Get(databaseRepo.WithAppInstallID(resourceApp.ID), commonRepo.WithByType(resource.Key), databaseRepo.WithByFrom(constant.AppResourceLocal), commonRepo.WithByName(resourceApp.Name)) + if err != nil { + return err + } + } + switch database.Type { + case constant.AppPostgresql: + db, err := postgresqlRepo.Get(commonRepo.WithByID(resource.ResourceId)) + if err != nil { + return err + } + if err := handlePostgresqlRecover(dto.CommonRecover{ + Name: database.Name, + DetailName: db.Name, + File: fmt.Sprintf("%s/%s.sql.gz", tmpPath, install.Name), + }, true); err != nil { + global.LOG.Errorf("handle recover from sql.gz failed, err: %v", err) + return err + } + case constant.AppMysql, constant.AppMariaDB: + db, err := mysqlRepo.Get(commonRepo.WithByID(resource.ResourceId)) + if err != nil { + return err + } + newDB, envMap, err := reCreateDB(db.ID, database, oldInstall.Env) + if err != nil { + return err + } + oldHost := fmt.Sprintf("\"PANEL_DB_HOST\":\"%v\"", envMap["PANEL_DB_HOST"].(string)) + newHost := fmt.Sprintf("\"PANEL_DB_HOST\":\"%v\"", database.Address) + oldInstall.Env = strings.ReplaceAll(oldInstall.Env, oldHost, newHost) + envMap["PANEL_DB_HOST"] = database.Address + newEnvFile, err = coverEnvJsonToStr(oldInstall.Env) + if err != nil { + return err + } + _ = appInstallResourceRepo.BatchUpdateBy(map[string]interface{}{"resource_id": newDB.ID}, commonRepo.WithByID(resource.ID)) + + if err := handleMysqlRecover(dto.CommonRecover{ + Name: newDB.MysqlName, + DetailName: newDB.Name, + File: fmt.Sprintf("%s/%s.sql.gz", tmpPath, install.Name), + }, true); err != nil { + global.LOG.Errorf("handle recover from sql.gz failed, err: %v", err) + return err + } + } + } + + appDir := install.GetPath() + backPath := fmt.Sprintf("%s_bak", appDir) + _ = fileOp.Rename(appDir, backPath) + _ = fileOp.CreateDir(appDir, 0755) + + if err := handleUnTar(tmpPath+"/app.tar.gz", install.GetAppPath(), ""); err != nil { + global.LOG.Errorf("handle recover from app.tar.gz failed, err: %v", err) + _ = fileOp.DeleteDir(appDir) + _ = fileOp.Rename(backPath, appDir) + return err + } + _ = fileOp.DeleteDir(backPath) + + if len(newEnvFile) != 0 { + envPath := fmt.Sprintf("%s/%s/.env", install.GetAppPath(), install.Name) + file, err := os.OpenFile(envPath, os.O_WRONLY|os.O_TRUNC, 0640) + if err != nil { + return err + } + defer file.Close() + _, _ = file.WriteString(newEnvFile) + } + + oldInstall.ID = install.ID + oldInstall.Status = constant.StatusRunning + oldInstall.AppId = install.AppId + oldInstall.AppDetailId = install.AppDetailId + oldInstall.App.ID = install.AppId + if err := appInstallRepo.Save(context.Background(), &oldInstall); err != nil { + global.LOG.Errorf("save db app install failed, err: %v", err) + return err + } + isOk = true + + return nil +} + +func reCreateDB(dbID uint, database model.Database, oldEnv string) (*model.DatabaseMysql, map[string]interface{}, error) { + mysqlService := NewIMysqlService() + ctx := context.Background() + _ = mysqlService.Delete(ctx, dto.MysqlDBDelete{ID: dbID, Database: database.Name, Type: database.Type, DeleteBackup: false, ForceDelete: true}) + + envMap := make(map[string]interface{}) + if err := json.Unmarshal([]byte(oldEnv), &envMap); err != nil { + return nil, envMap, err + } + oldName, _ := envMap["PANEL_DB_NAME"].(string) + oldUser, _ := envMap["PANEL_DB_USER"].(string) + oldPassword, _ := envMap["PANEL_DB_USER_PASSWORD"].(string) + createDB, err := mysqlService.Create(context.Background(), dto.MysqlDBCreate{ + Name: oldName, + From: database.From, + Database: database.Name, + Format: "utf8mb4", + Username: oldUser, + Password: oldPassword, + Permission: "%", + }) + cronjobs, _ := cronjobRepo.List(cronjobRepo.WithByDbName(fmt.Sprintf("%v", dbID))) + for _, job := range cronjobs { + _ = cronjobRepo.Update(job.ID, map[string]interface{}{"db_name": fmt.Sprintf("%v", createDB.ID)}) + } + if err != nil { + return nil, envMap, err + } + return createDB, envMap, nil +} diff --git a/agent/app/service/backup_mysql.go b/agent/app/service/backup_mysql.go new file mode 100644 index 000000000..e6bb3f5ba --- /dev/null +++ b/agent/app/service/backup_mysql.go @@ -0,0 +1,194 @@ +package service + +import ( + "fmt" + "os" + "path" + "path/filepath" + "strings" + "time" + + "github.com/1Panel-dev/1Panel/agent/constant" + + "github.com/1Panel-dev/1Panel/agent/buserr" + + "github.com/1Panel-dev/1Panel/agent/app/dto" + "github.com/1Panel-dev/1Panel/agent/app/model" + "github.com/1Panel-dev/1Panel/agent/global" + "github.com/1Panel-dev/1Panel/agent/utils/common" + "github.com/1Panel-dev/1Panel/agent/utils/files" + "github.com/1Panel-dev/1Panel/agent/utils/mysql/client" +) + +func (u *BackupService) MysqlBackup(req dto.CommonBackup) error { + localDir, err := loadLocalDir() + if err != nil { + return err + } + + timeNow := time.Now().Format(constant.DateTimeSlimLayout) + itemDir := fmt.Sprintf("database/%s/%s/%s", req.Type, req.Name, req.DetailName) + targetDir := path.Join(localDir, itemDir) + fileName := fmt.Sprintf("%s_%s.sql.gz", req.DetailName, timeNow+common.RandStrAndNum(5)) + + if err := handleMysqlBackup(req.Name, req.Type, req.DetailName, targetDir, fileName); err != nil { + return err + } + + record := &model.BackupRecord{ + Type: req.Type, + Name: req.Name, + DetailName: req.DetailName, + Source: "LOCAL", + BackupType: "LOCAL", + FileDir: itemDir, + FileName: fileName, + } + if err := backupRepo.CreateRecord(record); err != nil { + global.LOG.Errorf("save backup record failed, err: %v", err) + } + return nil +} + +func (u *BackupService) MysqlRecover(req dto.CommonRecover) error { + if err := handleMysqlRecover(req, false); err != nil { + return err + } + return nil +} + +func (u *BackupService) MysqlRecoverByUpload(req dto.CommonRecover) error { + file := req.File + fileName := path.Base(req.File) + if strings.HasSuffix(fileName, ".tar.gz") { + fileNameItem := time.Now().Format(constant.DateTimeSlimLayout) + dstDir := fmt.Sprintf("%s/%s", path.Dir(req.File), fileNameItem) + if _, err := os.Stat(dstDir); err != nil && os.IsNotExist(err) { + if err = os.MkdirAll(dstDir, os.ModePerm); err != nil { + return fmt.Errorf("mkdir %s failed, err: %v", dstDir, err) + } + } + if err := handleUnTar(req.File, dstDir, ""); err != nil { + _ = os.RemoveAll(dstDir) + return err + } + global.LOG.Infof("decompress file %s successful, now start to check test.sql is exist", req.File) + hasTestSql := false + _ = filepath.Walk(dstDir, func(path string, info os.FileInfo, err error) error { + if err != nil { + return nil + } + if !info.IsDir() && info.Name() == "test.sql" { + hasTestSql = true + file = path + fileName = "test.sql" + } + return nil + }) + if !hasTestSql { + _ = os.RemoveAll(dstDir) + return fmt.Errorf("no such file named test.sql in %s", fileName) + } + defer func() { + _ = os.RemoveAll(dstDir) + }() + } + + req.File = path.Dir(file) + "/" + fileName + if err := handleMysqlRecover(req, false); err != nil { + return err + } + global.LOG.Info("recover from uploads successful!") + return nil +} + +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, 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, + + Timeout: 300, + } + if err := cli.Backup(backupInfo); err != nil { + return err + } + return nil +} + +func handleMysqlRecover(req dto.CommonRecover, isRollback bool) error { + isOk := false + fileOp := files.NewFileOp() + if !fileOp.Stat(req.File) { + return buserr.WithName("ErrFileNotFound", req.File) + } + dbInfo, err := mysqlRepo.Get(commonRepo.WithByName(req.DetailName), mysqlRepo.WithByMysqlName(req.Name)) + if err != nil { + return err + } + cli, version, err := LoadMysqlClientByFrom(req.Name) + if err != nil { + return err + } + + if !isRollback { + rollbackFile := path.Join(global.CONF.System.TmpDir, fmt.Sprintf("database/%s/%s_%s.sql.gz", req.Type, req.DetailName, time.Now().Format(constant.DateTimeSlimLayout))) + 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), + + Timeout: 300, + }); err != nil { + return fmt.Errorf("backup mysql db %s for rollback before recover failed, err: %v", req.DetailName, err) + } + defer func() { + if !isOk { + 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, + + Timeout: 300, + }); err != nil { + global.LOG.Errorf("rollback mysql db %s from %s failed, err: %v", req.DetailName, rollbackFile, err) + } + global.LOG.Infof("rollback mysql db %s from %s successful", req.DetailName, rollbackFile) + _ = os.RemoveAll(rollbackFile) + } else { + _ = os.RemoveAll(rollbackFile) + } + }() + } + if err := cli.Recover(client.RecoverInfo{ + Name: req.DetailName, + Type: req.Type, + Version: version, + Format: dbInfo.Format, + SourceFile: req.File, + + Timeout: 300, + }); err != nil { + return err + } + isOk = true + return nil +} diff --git a/agent/app/service/backup_postgresql.go b/agent/app/service/backup_postgresql.go new file mode 100644 index 000000000..a0c171695 --- /dev/null +++ b/agent/app/service/backup_postgresql.go @@ -0,0 +1,179 @@ +package service + +import ( + "fmt" + "os" + "path" + "path/filepath" + "strings" + "time" + + "github.com/1Panel-dev/1Panel/agent/constant" + + "github.com/1Panel-dev/1Panel/agent/buserr" + "github.com/1Panel-dev/1Panel/agent/utils/common" + pgclient "github.com/1Panel-dev/1Panel/agent/utils/postgresql/client" + + "github.com/1Panel-dev/1Panel/agent/app/dto" + "github.com/1Panel-dev/1Panel/agent/app/model" + "github.com/1Panel-dev/1Panel/agent/global" + "github.com/1Panel-dev/1Panel/agent/utils/files" + "github.com/1Panel-dev/1Panel/agent/utils/postgresql/client" +) + +func (u *BackupService) PostgresqlBackup(req dto.CommonBackup) error { + localDir, err := loadLocalDir() + if err != nil { + return err + } + + timeNow := time.Now().Format(constant.DateTimeSlimLayout) + itemDir := fmt.Sprintf("database/%s/%s/%s", req.Type, req.Name, req.DetailName) + targetDir := path.Join(localDir, itemDir) + fileName := fmt.Sprintf("%s_%s.sql.gz", req.DetailName, timeNow+common.RandStrAndNum(5)) + + if err := handlePostgresqlBackup(req.Name, req.DetailName, targetDir, fileName); err != nil { + return err + } + + record := &model.BackupRecord{ + Type: req.Type, + Name: req.Name, + DetailName: req.DetailName, + Source: "LOCAL", + BackupType: "LOCAL", + FileDir: itemDir, + FileName: fileName, + } + if err := backupRepo.CreateRecord(record); err != nil { + global.LOG.Errorf("save backup record failed, err: %v", err) + } + return nil +} +func (u *BackupService) PostgresqlRecover(req dto.CommonRecover) error { + if err := handlePostgresqlRecover(req, false); err != nil { + return err + } + return nil +} + +func (u *BackupService) PostgresqlRecoverByUpload(req dto.CommonRecover) error { + file := req.File + fileName := path.Base(req.File) + if strings.HasSuffix(fileName, ".tar.gz") { + fileNameItem := time.Now().Format(constant.DateTimeSlimLayout) + dstDir := fmt.Sprintf("%s/%s", path.Dir(req.File), fileNameItem) + if _, err := os.Stat(dstDir); err != nil && os.IsNotExist(err) { + if err = os.MkdirAll(dstDir, os.ModePerm); err != nil { + return fmt.Errorf("mkdir %s failed, err: %v", dstDir, err) + } + } + if err := handleUnTar(req.File, dstDir, ""); err != nil { + _ = os.RemoveAll(dstDir) + return err + } + global.LOG.Infof("decompress file %s successful, now start to check test.sql is exist", req.File) + hasTestSql := false + _ = filepath.Walk(dstDir, func(path string, info os.FileInfo, err error) error { + if err != nil { + return nil + } + if !info.IsDir() && info.Name() == "test.sql" { + hasTestSql = true + file = path + fileName = "test.sql" + } + return nil + }) + if !hasTestSql { + _ = os.RemoveAll(dstDir) + return fmt.Errorf("no such file named test.sql in %s", fileName) + } + defer func() { + _ = os.RemoveAll(dstDir) + }() + } + + req.File = path.Dir(file) + "/" + fileName + if err := handlePostgresqlRecover(req, false); err != nil { + return err + } + global.LOG.Info("recover from uploads successful!") + return nil +} +func handlePostgresqlBackup(database, dbName, targetDir, fileName string) error { + cli, err := LoadPostgresqlClientByFrom(database) + if err != nil { + return err + } + defer cli.Close() + + backupInfo := pgclient.BackupInfo{ + Name: dbName, + TargetDir: targetDir, + FileName: fileName, + + Timeout: 300, + } + if err := cli.Backup(backupInfo); err != nil { + return err + } + return nil +} + +func handlePostgresqlRecover(req dto.CommonRecover, isRollback bool) error { + isOk := false + fileOp := files.NewFileOp() + if !fileOp.Stat(req.File) { + return buserr.WithName("ErrFileNotFound", req.File) + } + dbInfo, err := postgresqlRepo.Get(commonRepo.WithByName(req.DetailName), postgresqlRepo.WithByPostgresqlName(req.Name)) + if err != nil { + return err + } + cli, err := LoadPostgresqlClientByFrom(req.Name) + if err != nil { + return err + } + defer cli.Close() + + if !isRollback { + rollbackFile := path.Join(global.CONF.System.TmpDir, fmt.Sprintf("database/%s/%s_%s.sql.gz", req.Type, req.DetailName, time.Now().Format(constant.DateTimeSlimLayout))) + if err := cli.Backup(client.BackupInfo{ + Name: req.DetailName, + TargetDir: path.Dir(rollbackFile), + FileName: path.Base(rollbackFile), + + Timeout: 300, + }); err != nil { + return fmt.Errorf("backup postgresql db %s for rollback before recover failed, err: %v", req.DetailName, err) + } + defer func() { + if !isOk { + global.LOG.Info("recover failed, start to rollback now") + if err := cli.Recover(client.RecoverInfo{ + Name: req.DetailName, + SourceFile: rollbackFile, + + Timeout: 300, + }); err != nil { + global.LOG.Errorf("rollback postgresql db %s from %s failed, err: %v", req.DetailName, rollbackFile, err) + } + global.LOG.Infof("rollback postgresql db %s from %s successful", req.DetailName, rollbackFile) + _ = os.RemoveAll(rollbackFile) + } else { + _ = os.RemoveAll(rollbackFile) + } + }() + } + if err := cli.Recover(client.RecoverInfo{ + Name: req.DetailName, + SourceFile: req.File, + Username: dbInfo.Username, + Timeout: 300, + }); err != nil { + return err + } + isOk = true + return nil +} diff --git a/agent/app/service/backup_redis.go b/agent/app/service/backup_redis.go new file mode 100644 index 000000000..99c7b92c4 --- /dev/null +++ b/agent/app/service/backup_redis.go @@ -0,0 +1,194 @@ +package service + +import ( + "fmt" + "os" + "path" + "strings" + "time" + + "github.com/1Panel-dev/1Panel/agent/app/dto" + "github.com/1Panel-dev/1Panel/agent/app/model" + "github.com/1Panel-dev/1Panel/agent/app/repo" + "github.com/1Panel-dev/1Panel/agent/buserr" + "github.com/1Panel-dev/1Panel/agent/constant" + "github.com/1Panel-dev/1Panel/agent/global" + "github.com/1Panel-dev/1Panel/agent/utils/cmd" + "github.com/1Panel-dev/1Panel/agent/utils/common" + "github.com/1Panel-dev/1Panel/agent/utils/compose" + "github.com/1Panel-dev/1Panel/agent/utils/files" + "github.com/pkg/errors" +) + +func (u *BackupService) RedisBackup(db dto.CommonBackup) error { + localDir, err := loadLocalDir() + if err != nil { + return err + } + redisInfo, err := appInstallRepo.LoadBaseInfo("redis", db.Name) + if err != nil { + return err + } + appendonly, err := configGetStr(redisInfo.ContainerName, redisInfo.Password, "appendonly") + if err != nil { + return err + } + global.LOG.Infof("appendonly in redis conf is %s", appendonly) + + timeNow := time.Now().Format(constant.DateTimeSlimLayout) + common.RandStrAndNum(5) + fileName := fmt.Sprintf("%s.rdb", timeNow) + if appendonly == "yes" { + if strings.HasPrefix(redisInfo.Version, "6.") { + fileName = fmt.Sprintf("%s.aof", timeNow) + } else { + fileName = fmt.Sprintf("%s.tar.gz", timeNow) + } + } + itemDir := fmt.Sprintf("database/redis/%s", redisInfo.Name) + backupDir := path.Join(localDir, itemDir) + if err := handleRedisBackup(redisInfo, backupDir, fileName, db.Secret); err != nil { + return err + } + record := &model.BackupRecord{ + Type: "redis", + Name: db.Name, + Source: "LOCAL", + BackupType: "LOCAL", + FileDir: itemDir, + FileName: fileName, + } + if err := backupRepo.CreateRecord(record); err != nil { + global.LOG.Errorf("save backup record failed, err: %v", err) + } + + return nil +} + +func (u *BackupService) RedisRecover(req dto.CommonRecover) error { + redisInfo, err := appInstallRepo.LoadBaseInfo("redis", req.Name) + if err != nil { + return err + } + global.LOG.Infof("recover redis from backup file %s", req.File) + if err := handleRedisRecover(redisInfo, req.File, false, req.Secret); err != nil { + return err + } + return nil +} + +func handleRedisBackup(redisInfo *repo.RootInfo, backupDir, fileName string, secret string) error { + fileOp := files.NewFileOp() + if !fileOp.Stat(backupDir) { + if err := os.MkdirAll(backupDir, os.ModePerm); err != nil { + return fmt.Errorf("mkdir %s failed, err: %v", backupDir, err) + } + } + + stdout, err := cmd.Execf("docker exec %s redis-cli -a %s --no-auth-warning save", redisInfo.ContainerName, redisInfo.Password) + if err != nil { + return errors.New(string(stdout)) + } + + if strings.HasSuffix(fileName, ".tar.gz") { + redisDataDir := fmt.Sprintf("%s/%s/%s/data/appendonlydir", constant.AppInstallDir, "redis", redisInfo.Name) + if err := handleTar(redisDataDir, backupDir, fileName, "", secret); err != nil { + return err + } + return nil + } + if strings.HasSuffix(fileName, ".aof") { + stdout1, err := cmd.Execf("docker cp %s:/data/appendonly.aof %s/%s", redisInfo.ContainerName, backupDir, fileName) + if err != nil { + return errors.New(string(stdout1)) + } + return nil + } + + stdout1, err1 := cmd.Execf("docker cp %s:/data/dump.rdb %s/%s", redisInfo.ContainerName, backupDir, fileName) + if err1 != nil { + return errors.New(string(stdout1)) + } + return nil +} + +func handleRedisRecover(redisInfo *repo.RootInfo, recoverFile string, isRollback bool, secret string) error { + fileOp := files.NewFileOp() + if !fileOp.Stat(recoverFile) { + return buserr.WithName("ErrFileNotFound", recoverFile) + } + + appendonly, err := configGetStr(redisInfo.ContainerName, redisInfo.Password, "appendonly") + if err != nil { + return err + } + + if appendonly == "yes" { + if strings.HasPrefix(redisInfo.Version, "6.") && !strings.HasSuffix(recoverFile, ".aof") { + return buserr.New(constant.ErrTypeOfRedis) + } + if strings.HasPrefix(redisInfo.Version, "7.") && !strings.HasSuffix(recoverFile, ".tar.gz") { + return buserr.New(constant.ErrTypeOfRedis) + } + } else { + if !strings.HasSuffix(recoverFile, ".rdb") { + return buserr.New(constant.ErrTypeOfRedis) + } + } + + global.LOG.Infof("appendonly in redis conf is %s", appendonly) + isOk := false + if !isRollback { + suffix := "rdb" + if appendonly == "yes" { + if strings.HasPrefix(redisInfo.Version, "6.") { + suffix = "aof" + } else { + suffix = "tar.gz" + } + } + rollbackFile := path.Join(global.CONF.System.TmpDir, fmt.Sprintf("database/redis/%s_%s.%s", redisInfo.Name, time.Now().Format(constant.DateTimeSlimLayout), suffix)) + if err := handleRedisBackup(redisInfo, path.Dir(rollbackFile), path.Base(rollbackFile), secret); err != nil { + return fmt.Errorf("backup database %s for rollback before recover failed, err: %v", redisInfo.Name, err) + } + defer func() { + if !isOk { + global.LOG.Info("recover failed, start to rollback now") + if err := handleRedisRecover(redisInfo, rollbackFile, true, secret); err != nil { + global.LOG.Errorf("rollback redis from %s failed, err: %v", rollbackFile, err) + return + } + global.LOG.Infof("rollback redis from %s successful", rollbackFile) + _ = os.RemoveAll(rollbackFile) + } else { + _ = os.RemoveAll(rollbackFile) + } + }() + } + composeDir := fmt.Sprintf("%s/redis/%s", constant.AppInstallDir, redisInfo.Name) + if _, err := compose.Down(composeDir + "/docker-compose.yml"); err != nil { + return err + } + if appendonly == "yes" && strings.HasPrefix(redisInfo.Version, "7.") { + redisDataDir := fmt.Sprintf("%s/%s/%s/data", constant.AppInstallDir, "redis", redisInfo.Name) + if err := handleUnTar(recoverFile, redisDataDir, secret); err != nil { + return err + } + } else { + itemName := "dump.rdb" + if appendonly == "yes" && strings.HasPrefix(redisInfo.Version, "6.") { + itemName = "appendonly.aof" + } + input, err := os.ReadFile(recoverFile) + if err != nil { + return err + } + if err = os.WriteFile(composeDir+"/data/"+itemName, input, 0640); err != nil { + return err + } + } + if _, err := compose.Up(composeDir + "/docker-compose.yml"); err != nil { + return err + } + isOk = true + return nil +} diff --git a/agent/app/service/backup_runtime.go b/agent/app/service/backup_runtime.go new file mode 100644 index 000000000..c19c2101a --- /dev/null +++ b/agent/app/service/backup_runtime.go @@ -0,0 +1,129 @@ +package service + +import ( + "encoding/json" + "errors" + "fmt" + "io/fs" + "os" + "path" + "strings" + "time" + + "github.com/1Panel-dev/1Panel/agent/app/model" + "github.com/1Panel-dev/1Panel/agent/constant" + "github.com/1Panel-dev/1Panel/agent/global" + "github.com/1Panel-dev/1Panel/agent/utils/files" +) + +func handleRuntimeBackup(runtime *model.Runtime, backupDir, fileName string, excludes string, secret string) error { + fileOp := files.NewFileOp() + tmpDir := fmt.Sprintf("%s/%s", backupDir, strings.ReplaceAll(fileName, ".tar.gz", "")) + if !fileOp.Stat(tmpDir) { + if err := os.MkdirAll(tmpDir, os.ModePerm); err != nil { + return fmt.Errorf("mkdir %s failed, err: %v", backupDir, err) + } + } + defer func() { + _ = os.RemoveAll(tmpDir) + }() + + remarkInfo, _ := json.Marshal(runtime) + remarkInfoPath := fmt.Sprintf("%s/runtime.json", tmpDir) + if err := fileOp.SaveFile(remarkInfoPath, string(remarkInfo), fs.ModePerm); err != nil { + return err + } + + appPath := runtime.GetPath() + if err := handleTar(appPath, tmpDir, "runtime.tar.gz", excludes, secret); err != nil { + return err + } + if err := handleTar(tmpDir, backupDir, fileName, "", secret); err != nil { + return err + } + return nil +} + +func handleRuntimeRecover(runtime *model.Runtime, recoverFile string, isRollback bool, secret string) error { + isOk := false + fileOp := files.NewFileOp() + if err := handleUnTar(recoverFile, path.Dir(recoverFile), secret); err != nil { + return err + } + tmpPath := strings.ReplaceAll(recoverFile, ".tar.gz", "") + defer func() { + go startRuntime(runtime) + _ = os.RemoveAll(strings.ReplaceAll(recoverFile, ".tar.gz", "")) + }() + + if !fileOp.Stat(tmpPath+"/runtime.json") || !fileOp.Stat(tmpPath+"/runtime.tar.gz") { + return errors.New("the wrong recovery package does not have runtime.json or runtime.tar.gz files") + } + var oldRuntime model.Runtime + runtimeJson, err := os.ReadFile(tmpPath + "/runtime.json") + if err != nil { + return err + } + if err := json.Unmarshal(runtimeJson, &oldRuntime); err != nil { + return fmt.Errorf("unmarshal runtime.json failed, err: %v", err) + } + if oldRuntime.Type != runtime.Type || oldRuntime.Name != runtime.Name { + return errors.New("the current backup file does not match the application") + } + + if !isRollback { + rollbackFile := path.Join(global.CONF.System.TmpDir, fmt.Sprintf("runtime/%s_%s.tar.gz", runtime.Name, time.Now().Format(constant.DateTimeSlimLayout))) + if err := handleRuntimeBackup(runtime, path.Dir(rollbackFile), path.Base(rollbackFile), "", secret); err != nil { + return fmt.Errorf("backup runtime %s for rollback before recover failed, err: %v", runtime.Name, err) + } + defer func() { + if !isOk { + global.LOG.Info("recover failed, start to rollback now") + if err := handleRuntimeRecover(runtime, rollbackFile, true, secret); err != nil { + global.LOG.Errorf("rollback runtime %s from %s failed, err: %v", runtime.Name, rollbackFile, err) + return + } + global.LOG.Infof("rollback runtime %s from %s successful", runtime.Name, rollbackFile) + _ = os.RemoveAll(rollbackFile) + } else { + _ = os.RemoveAll(rollbackFile) + } + }() + } + + newEnvFile, err := coverEnvJsonToStr(runtime.Env) + if err != nil { + return err + } + runtimeDir := runtime.GetPath() + backPath := fmt.Sprintf("%s_bak", runtimeDir) + _ = fileOp.Rename(runtimeDir, backPath) + _ = fileOp.CreateDir(runtimeDir, 0755) + + if err := handleUnTar(tmpPath+"/runtime.tar.gz", fmt.Sprintf("%s/%s", constant.RuntimeDir, runtime.Type), secret); err != nil { + global.LOG.Errorf("handle recover from runtime.tar.gz failed, err: %v", err) + _ = fileOp.DeleteDir(runtimeDir) + _ = fileOp.Rename(backPath, runtimeDir) + return err + } + _ = fileOp.DeleteDir(backPath) + + if len(newEnvFile) != 0 { + envPath := fmt.Sprintf("%s/%s/%s/.env", constant.RuntimeDir, runtime.Type, runtime.Name) + file, err := os.OpenFile(envPath, os.O_WRONLY|os.O_TRUNC, 0640) + if err != nil { + return err + } + defer file.Close() + _, _ = file.WriteString(newEnvFile) + } + + oldRuntime.ID = runtime.ID + oldRuntime.Status = constant.RuntimeStarting + if err := runtimeRepo.Save(&oldRuntime); err != nil { + global.LOG.Errorf("save db app install failed, err: %v", err) + return err + } + isOk = true + return nil +} diff --git a/agent/app/service/backup_website.go b/agent/app/service/backup_website.go new file mode 100644 index 000000000..344af6520 --- /dev/null +++ b/agent/app/service/backup_website.go @@ -0,0 +1,269 @@ +package service + +import ( + "encoding/json" + "fmt" + "io/fs" + "os" + "path" + "strings" + "time" + + "github.com/1Panel-dev/1Panel/agent/app/dto" + "github.com/1Panel-dev/1Panel/agent/app/model" + "github.com/1Panel-dev/1Panel/agent/buserr" + "github.com/1Panel-dev/1Panel/agent/constant" + "github.com/1Panel-dev/1Panel/agent/global" + "github.com/1Panel-dev/1Panel/agent/utils/cmd" + "github.com/1Panel-dev/1Panel/agent/utils/common" + "github.com/1Panel-dev/1Panel/agent/utils/compose" + "github.com/1Panel-dev/1Panel/agent/utils/files" + "github.com/pkg/errors" +) + +func (u *BackupService) WebsiteBackup(req dto.CommonBackup) error { + localDir, err := loadLocalDir() + if err != nil { + return err + } + website, err := websiteRepo.GetFirst(websiteRepo.WithAlias(req.DetailName)) + if err != nil { + return err + } + + timeNow := time.Now().Format(constant.DateTimeSlimLayout) + itemDir := fmt.Sprintf("website/%s", req.Name) + backupDir := path.Join(localDir, itemDir) + fileName := fmt.Sprintf("%s_%s.tar.gz", website.PrimaryDomain, timeNow+common.RandStrAndNum(5)) + if err := handleWebsiteBackup(&website, backupDir, fileName, "", req.Secret); err != nil { + return err + } + + record := &model.BackupRecord{ + Type: "website", + Name: website.PrimaryDomain, + DetailName: req.DetailName, + Source: "LOCAL", + BackupType: "LOCAL", + FileDir: itemDir, + FileName: fileName, + } + if err := backupRepo.CreateRecord(record); err != nil { + global.LOG.Errorf("save backup record failed, err: %v", err) + return err + } + return nil +} + +func (u *BackupService) WebsiteRecover(req dto.CommonRecover) error { + fileOp := files.NewFileOp() + if !fileOp.Stat(req.File) { + return buserr.WithName("ErrFileNotFound", req.File) + } + website, err := websiteRepo.GetFirst(websiteRepo.WithAlias(req.DetailName)) + if err != nil { + return err + } + global.LOG.Infof("recover website %s from backup file %s", req.Name, req.File) + if err := handleWebsiteRecover(&website, req.File, false, req.Secret); err != nil { + return err + } + return nil +} + +func handleWebsiteRecover(website *model.Website, recoverFile string, isRollback bool, secret string) error { + fileOp := files.NewFileOp() + tmpPath := strings.ReplaceAll(recoverFile, ".tar.gz", "") + if err := handleUnTar(recoverFile, path.Dir(recoverFile), secret); err != nil { + return err + } + defer func() { + _ = os.RemoveAll(tmpPath) + }() + + var oldWebsite model.Website + websiteJson, err := os.ReadFile(tmpPath + "/website.json") + if err != nil { + return err + } + if err := json.Unmarshal(websiteJson, &oldWebsite); err != nil { + return fmt.Errorf("unmarshal app.json failed, err: %v", err) + } + + if err := checkValidOfWebsite(&oldWebsite, website); err != nil { + return err + } + + temPathWithName := tmpPath + "/" + website.Alias + if !fileOp.Stat(tmpPath+"/website.json") || !fileOp.Stat(temPathWithName+".conf") || !fileOp.Stat(temPathWithName+".web.tar.gz") { + return buserr.WithDetail(constant.ErrBackupExist, ".conf or .web.tar.gz", nil) + } + if website.Type == constant.Deployment { + if !fileOp.Stat(temPathWithName + ".app.tar.gz") { + return buserr.WithDetail(constant.ErrBackupExist, ".app.tar.gz", nil) + } + } + + isOk := false + if !isRollback { + rollbackFile := path.Join(global.CONF.System.TmpDir, fmt.Sprintf("website/%s_%s.tar.gz", website.Alias, time.Now().Format(constant.DateTimeSlimLayout))) + if err := handleWebsiteBackup(website, path.Dir(rollbackFile), path.Base(rollbackFile), "", ""); err != nil { + return fmt.Errorf("backup website %s for rollback before recover failed, err: %v", website.Alias, err) + } + defer func() { + if !isOk { + global.LOG.Info("recover failed, start to rollback now") + if err := handleWebsiteRecover(website, rollbackFile, true, ""); err != nil { + global.LOG.Errorf("rollback website %s from %s failed, err: %v", website.Alias, rollbackFile, err) + return + } + global.LOG.Infof("rollback website %s from %s successful", website.Alias, rollbackFile) + _ = os.RemoveAll(rollbackFile) + } else { + _ = os.RemoveAll(rollbackFile) + } + }() + } + + nginxInfo, err := appInstallRepo.LoadBaseInfo(constant.AppOpenresty, "") + if err != nil { + return err + } + nginxConfPath := fmt.Sprintf("%s/openresty/%s/conf/conf.d", constant.AppInstallDir, nginxInfo.Name) + if err := fileOp.CopyFile(fmt.Sprintf("%s/%s.conf", tmpPath, website.Alias), nginxConfPath); err != nil { + global.LOG.Errorf("handle recover from conf.d failed, err: %v", err) + return err + } + + switch website.Type { + case constant.Deployment: + app, err := appInstallRepo.GetFirst(commonRepo.WithByID(website.AppInstallID)) + if err != nil { + return err + } + if err := handleAppRecover(&app, fmt.Sprintf("%s/%s.app.tar.gz", tmpPath, website.Alias), true, ""); err != nil { + global.LOG.Errorf("handle recover from app.tar.gz failed, err: %v", err) + return err + } + if _, err := compose.Restart(fmt.Sprintf("%s/%s/%s/docker-compose.yml", constant.AppInstallDir, app.App.Key, app.Name)); err != nil { + global.LOG.Errorf("docker-compose restart failed, err: %v", err) + return err + } + case constant.Runtime: + runtime, err := runtimeRepo.GetFirst(commonRepo.WithByID(website.RuntimeID)) + if err != nil { + return err + } + if runtime.Type == constant.RuntimeNode || runtime.Type == constant.RuntimeJava || runtime.Type == constant.RuntimeGo { + if err := handleRuntimeRecover(runtime, fmt.Sprintf("%s/%s.runtime.tar.gz", tmpPath, website.Alias), true, ""); err != nil { + return err + } + global.LOG.Info("put runtime.tar.gz into tmp dir successful") + } + } + + siteDir := fmt.Sprintf("%s/openresty/%s/www/sites", constant.AppInstallDir, nginxInfo.Name) + if err := handleUnTar(fmt.Sprintf("%s/%s.web.tar.gz", tmpPath, website.Alias), siteDir, ""); err != nil { + global.LOG.Errorf("handle recover from web.tar.gz failed, err: %v", err) + return err + } + stdout, err := cmd.Execf("docker exec -i %s nginx -s reload", nginxInfo.ContainerName) + if err != nil { + global.LOG.Errorf("nginx -s reload failed, err: %s", stdout) + return errors.New(string(stdout)) + } + + oldWebsite.ID = website.ID + if err := websiteRepo.SaveWithoutCtx(&oldWebsite); err != nil { + global.LOG.Errorf("handle save website data failed, err: %v", err) + return err + } + isOk = true + return nil +} + +func handleWebsiteBackup(website *model.Website, backupDir, fileName string, excludes string, secret string) error { + fileOp := files.NewFileOp() + tmpDir := fmt.Sprintf("%s/%s", backupDir, strings.ReplaceAll(fileName, ".tar.gz", "")) + if !fileOp.Stat(tmpDir) { + if err := os.MkdirAll(tmpDir, os.ModePerm); err != nil { + return fmt.Errorf("mkdir %s failed, err: %v", backupDir, err) + } + } + defer func() { + _ = os.RemoveAll(tmpDir) + }() + + remarkInfo, _ := json.Marshal(website) + if err := fileOp.SaveFile(tmpDir+"/website.json", string(remarkInfo), fs.ModePerm); err != nil { + return err + } + global.LOG.Info("put website.json into tmp dir successful") + + nginxInfo, err := appInstallRepo.LoadBaseInfo(constant.AppOpenresty, "") + if err != nil { + return err + } + nginxConfFile := fmt.Sprintf("%s/openresty/%s/conf/conf.d/%s.conf", constant.AppInstallDir, nginxInfo.Name, website.Alias) + if err := fileOp.CopyFile(nginxConfFile, tmpDir); err != nil { + return err + } + global.LOG.Info("put openresty conf into tmp dir successful") + + switch website.Type { + case constant.Deployment: + app, err := appInstallRepo.GetFirst(commonRepo.WithByID(website.AppInstallID)) + if err != nil { + return err + } + if err := handleAppBackup(&app, tmpDir, fmt.Sprintf("%s.app.tar.gz", website.Alias), excludes, ""); err != nil { + return err + } + global.LOG.Info("put app.tar.gz into tmp dir successful") + case constant.Runtime: + runtime, err := runtimeRepo.GetFirst(commonRepo.WithByID(website.RuntimeID)) + if err != nil { + return err + } + if runtime.Type == constant.RuntimeNode || runtime.Type == constant.RuntimeJava || runtime.Type == constant.RuntimeGo { + if err := handleRuntimeBackup(runtime, tmpDir, fmt.Sprintf("%s.runtime.tar.gz", website.Alias), excludes, ""); err != nil { + return err + } + global.LOG.Info("put runtime.tar.gz into tmp dir successful") + } + } + + websiteDir := fmt.Sprintf("%s/openresty/%s/www/sites/%s", constant.AppInstallDir, nginxInfo.Name, website.Alias) + if err := handleTar(websiteDir, tmpDir, fmt.Sprintf("%s.web.tar.gz", website.Alias), excludes, ""); err != nil { + return err + } + global.LOG.Info("put web.tar.gz into tmp dir successful, now start to tar tmp dir") + if err := handleTar(tmpDir, backupDir, fileName, "", secret); err != nil { + return err + } + + return nil +} + +func checkValidOfWebsite(oldWebsite, website *model.Website) error { + if oldWebsite.Alias != website.Alias || oldWebsite.Type != website.Type { + return buserr.WithDetail(constant.ErrBackupMatch, fmt.Sprintf("oldName: %s, oldType: %v", oldWebsite.Alias, oldWebsite.Type), nil) + } + if oldWebsite.AppInstallID != 0 { + _, err := appInstallRepo.GetFirst(commonRepo.WithByID(oldWebsite.AppInstallID)) + if err != nil { + return buserr.WithDetail(constant.ErrBackupMatch, "app", nil) + } + } + if oldWebsite.RuntimeID != 0 { + if _, err := runtimeRepo.GetFirst(commonRepo.WithByID(oldWebsite.RuntimeID)); err != nil { + return buserr.WithDetail(constant.ErrBackupMatch, "runtime", nil) + } + } + if oldWebsite.WebsiteSSLID != 0 { + if _, err := websiteSSLRepo.GetFirst(commonRepo.WithByID(oldWebsite.WebsiteSSLID)); err != nil { + return buserr.WithDetail(constant.ErrBackupMatch, "ssl", nil) + } + } + return nil +} diff --git a/agent/app/service/clam.go b/agent/app/service/clam.go new file mode 100644 index 000000000..41dcf84c8 --- /dev/null +++ b/agent/app/service/clam.go @@ -0,0 +1,562 @@ +package service + +import ( + "bufio" + "fmt" + "os" + "os/exec" + "path" + "path/filepath" + "sort" + "strings" + "time" + + "github.com/1Panel-dev/1Panel/agent/app/dto" + "github.com/1Panel-dev/1Panel/agent/app/model" + "github.com/1Panel-dev/1Panel/agent/buserr" + "github.com/1Panel-dev/1Panel/agent/constant" + "github.com/1Panel-dev/1Panel/agent/global" + "github.com/1Panel-dev/1Panel/agent/utils/cmd" + "github.com/1Panel-dev/1Panel/agent/utils/common" + "github.com/1Panel-dev/1Panel/agent/utils/systemctl" + "github.com/1Panel-dev/1Panel/agent/utils/xpack" + "github.com/jinzhu/copier" + "github.com/robfig/cron/v3" + + "github.com/pkg/errors" +) + +const ( + clamServiceNameCentOs = "clamd@scan.service" + clamServiceNameUbuntu = "clamav-daemon.service" + freshClamService = "clamav-freshclam.service" + resultDir = "clamav" +) + +type ClamService struct { + serviceName string +} + +type IClamService interface { + LoadBaseInfo() (dto.ClamBaseInfo, error) + Operate(operate string) error + SearchWithPage(search dto.SearchClamWithPage) (int64, interface{}, error) + Create(req dto.ClamCreate) error + Update(req dto.ClamUpdate) error + UpdateStatus(id uint, status string) error + Delete(req dto.ClamDelete) error + HandleOnce(req dto.OperateByID) error + LoadFile(req dto.ClamFileReq) (string, error) + UpdateFile(req dto.UpdateByNameAndFile) error + LoadRecords(req dto.ClamLogSearch) (int64, interface{}, error) + CleanRecord(req dto.OperateByID) error + + LoadRecordLog(req dto.ClamLogReq) (string, error) +} + +func NewIClamService() IClamService { + return &ClamService{} +} + +func (c *ClamService) LoadBaseInfo() (dto.ClamBaseInfo, error) { + var baseInfo dto.ClamBaseInfo + baseInfo.Version = "-" + baseInfo.FreshVersion = "-" + exist1, _ := systemctl.IsExist(clamServiceNameCentOs) + if exist1 { + c.serviceName = clamServiceNameCentOs + baseInfo.IsExist = true + baseInfo.IsActive, _ = systemctl.IsActive(clamServiceNameCentOs) + } + exist2, _ := systemctl.IsExist(clamServiceNameUbuntu) + if exist2 { + c.serviceName = clamServiceNameUbuntu + baseInfo.IsExist = true + baseInfo.IsActive, _ = systemctl.IsActive(clamServiceNameUbuntu) + } + freshExist, _ := systemctl.IsExist(freshClamService) + if freshExist { + baseInfo.FreshIsExist = true + baseInfo.FreshIsActive, _ = systemctl.IsActive(freshClamService) + } + if !cmd.Which("clamdscan") { + baseInfo.IsActive = false + } + + if baseInfo.IsActive { + version, err := cmd.Exec("clamdscan --version") + if err == nil { + if strings.Contains(version, "/") { + baseInfo.Version = strings.TrimPrefix(strings.Split(version, "/")[0], "ClamAV ") + } else { + baseInfo.Version = strings.TrimPrefix(version, "ClamAV ") + } + } + } + if baseInfo.FreshIsActive { + version, err := cmd.Exec("freshclam --version") + if err == nil { + if strings.Contains(version, "/") { + baseInfo.FreshVersion = strings.TrimPrefix(strings.Split(version, "/")[0], "ClamAV ") + } else { + baseInfo.FreshVersion = strings.TrimPrefix(version, "ClamAV ") + } + } + } + return baseInfo, nil +} + +func (c *ClamService) Operate(operate string) error { + switch operate { + case "start", "restart", "stop": + stdout, err := cmd.Execf("systemctl %s %s", operate, c.serviceName) + if err != nil { + return fmt.Errorf("%s the %s failed, err: %s", operate, c.serviceName, stdout) + } + return nil + case "fresh-start", "fresh-restart", "fresh-stop": + stdout, err := cmd.Execf("systemctl %s %s", strings.TrimPrefix(operate, "fresh-"), freshClamService) + if err != nil { + return fmt.Errorf("%s the %s failed, err: %s", operate, c.serviceName, stdout) + } + return nil + default: + return fmt.Errorf("not support such operation: %v", operate) + } +} + +func (c *ClamService) SearchWithPage(req dto.SearchClamWithPage) (int64, interface{}, error) { + total, commands, err := clamRepo.Page(req.Page, req.PageSize, commonRepo.WithLikeName(req.Info), commonRepo.WithOrderRuleBy(req.OrderBy, req.Order)) + if err != nil { + return 0, nil, err + } + var datas []dto.ClamInfo + for _, command := range commands { + var item dto.ClamInfo + if err := copier.Copy(&item, &command); err != nil { + return 0, nil, errors.WithMessage(constant.ErrStructTransform, err.Error()) + } + item.LastHandleDate = "-" + datas = append(datas, item) + } + nyc, _ := time.LoadLocation(common.LoadTimeZone()) + for i := 0; i < len(datas); i++ { + logPaths := loadFileByName(datas[i].Name) + sort.Slice(logPaths, func(i, j int) bool { + return logPaths[i] > logPaths[j] + }) + if len(logPaths) != 0 { + t1, err := time.ParseInLocation(constant.DateTimeSlimLayout, logPaths[0], nyc) + if err != nil { + continue + } + datas[i].LastHandleDate = t1.Format(constant.DateTimeLayout) + } + } + return total, datas, err +} + +func (c *ClamService) Create(req dto.ClamCreate) error { + clam, _ := clamRepo.Get(commonRepo.WithByName(req.Name)) + if clam.ID != 0 { + return constant.ErrRecordExist + } + if err := copier.Copy(&clam, &req); err != nil { + return errors.WithMessage(constant.ErrStructTransform, err.Error()) + } + if clam.InfectedStrategy == "none" || clam.InfectedStrategy == "remove" { + clam.InfectedDir = "" + } + if len(req.Spec) != 0 { + entryID, err := xpack.StartClam(clam, false) + if err != nil { + return err + } + clam.EntryID = entryID + clam.Status = constant.StatusEnable + } + if err := clamRepo.Create(&clam); err != nil { + return err + } + return nil +} + +func (c *ClamService) Update(req dto.ClamUpdate) error { + clam, _ := clamRepo.Get(commonRepo.WithByName(req.Name)) + if clam.ID == 0 { + return constant.ErrRecordNotFound + } + if req.InfectedStrategy == "none" || req.InfectedStrategy == "remove" { + req.InfectedDir = "" + } + var clamItem model.Clam + if err := copier.Copy(&clamItem, &req); err != nil { + return errors.WithMessage(constant.ErrStructTransform, err.Error()) + } + clamItem.EntryID = clam.EntryID + upMap := map[string]interface{}{} + if len(clam.Spec) != 0 && clam.EntryID != 0 { + global.Cron.Remove(cron.EntryID(clamItem.EntryID)) + upMap["entry_id"] = 0 + } + if len(req.Spec) == 0 { + upMap["status"] = "" + upMap["entry_id"] = 0 + } + if len(req.Spec) != 0 && clam.Status != constant.StatusDisable { + newEntryID, err := xpack.StartClam(clamItem, true) + if err != nil { + return err + } + upMap["entry_id"] = newEntryID + } + if len(clam.Spec) == 0 && len(req.Spec) != 0 { + upMap["status"] = constant.StatusEnable + } + + upMap["name"] = req.Name + upMap["path"] = req.Path + upMap["infected_dir"] = req.InfectedDir + upMap["infected_strategy"] = req.InfectedStrategy + upMap["spec"] = req.Spec + upMap["description"] = req.Description + if err := clamRepo.Update(req.ID, upMap); err != nil { + return err + } + return nil +} + +func (c *ClamService) UpdateStatus(id uint, status string) error { + clam, _ := clamRepo.Get(commonRepo.WithByID(id)) + if clam.ID == 0 { + return constant.ErrRecordNotFound + } + var ( + entryID int + err error + ) + if status == constant.StatusEnable { + entryID, err = xpack.StartClam(clam, true) + if err != nil { + return err + } + } else { + global.Cron.Remove(cron.EntryID(clam.EntryID)) + global.LOG.Infof("stop cronjob entryID: %v", clam.EntryID) + } + + return clamRepo.Update(clam.ID, map[string]interface{}{"status": status, "entry_id": entryID}) +} + +func (c *ClamService) Delete(req dto.ClamDelete) error { + for _, id := range req.Ids { + clam, _ := clamRepo.Get(commonRepo.WithByID(id)) + if clam.ID == 0 { + continue + } + if req.RemoveRecord { + _ = os.RemoveAll(path.Join(global.CONF.System.DataDir, resultDir, clam.Name)) + } + if req.RemoveInfected { + _ = os.RemoveAll(path.Join(clam.InfectedDir, "1panel-infected", clam.Name)) + } + if err := clamRepo.Delete(commonRepo.WithByID(id)); err != nil { + return err + } + } + return nil +} + +func (c *ClamService) HandleOnce(req dto.OperateByID) error { + if !cmd.Which("clamdscan") { + return buserr.New("ErrClamdscanNotFound") + } + clam, _ := clamRepo.Get(commonRepo.WithByID(req.ID)) + if clam.ID == 0 { + return constant.ErrRecordNotFound + } + if cmd.CheckIllegal(clam.Path) { + return buserr.New(constant.ErrCmdIllegal) + } + timeNow := time.Now().Format(constant.DateTimeSlimLayout) + logFile := path.Join(global.CONF.System.DataDir, resultDir, clam.Name, timeNow) + if _, err := os.Stat(path.Dir(logFile)); err != nil { + _ = os.MkdirAll(path.Dir(logFile), os.ModePerm) + } + go func() { + strategy := "" + switch clam.InfectedStrategy { + case "remove": + strategy = "--remove" + case "move": + dir := path.Join(clam.InfectedDir, "1panel-infected", clam.Name, timeNow) + strategy = "--move=" + dir + if _, err := os.Stat(dir); err != nil { + _ = os.MkdirAll(dir, os.ModePerm) + } + case "copy": + dir := path.Join(clam.InfectedDir, "1panel-infected", clam.Name, timeNow) + strategy = "--copy=" + dir + if _, err := os.Stat(dir); err != nil { + _ = os.MkdirAll(dir, os.ModePerm) + } + } + global.LOG.Debugf("clamdscan --fdpass %s %s -l %s", strategy, clam.Path, logFile) + stdout, err := cmd.Execf("clamdscan --fdpass %s %s -l %s", strategy, clam.Path, logFile) + if err != nil { + global.LOG.Errorf("clamdscan failed, stdout: %v, err: %v", stdout, err) + } + }() + return nil +} + +func (c *ClamService) LoadRecords(req dto.ClamLogSearch) (int64, interface{}, error) { + clam, _ := clamRepo.Get(commonRepo.WithByID(req.ClamID)) + if clam.ID == 0 { + return 0, nil, constant.ErrRecordNotFound + } + logPaths := loadFileByName(clam.Name) + if len(logPaths) == 0 { + return 0, nil, nil + } + + var filterFiles []string + nyc, _ := time.LoadLocation(common.LoadTimeZone()) + for _, item := range logPaths { + t1, err := time.ParseInLocation(constant.DateTimeSlimLayout, item, nyc) + if err != nil { + continue + } + if t1.After(req.StartTime) && t1.Before(req.EndTime) { + filterFiles = append(filterFiles, item) + } + } + if len(filterFiles) == 0 { + return 0, nil, nil + } + + sort.Slice(filterFiles, func(i, j int) bool { + return filterFiles[i] > filterFiles[j] + }) + + var records []string + total, start, end := len(filterFiles), (req.Page-1)*req.PageSize, req.Page*req.PageSize + if start > total { + records = make([]string, 0) + } else { + if end >= total { + end = total + } + records = filterFiles[start:end] + } + + var datas []dto.ClamLog + for i := 0; i < len(records); i++ { + item := loadResultFromLog(path.Join(global.CONF.System.DataDir, resultDir, clam.Name, records[i])) + datas = append(datas, item) + } + return int64(total), datas, nil +} +func (c *ClamService) LoadRecordLog(req dto.ClamLogReq) (string, error) { + logPath := path.Join(global.CONF.System.DataDir, resultDir, req.ClamName, req.RecordName) + var tail string + if req.Tail != "0" { + tail = req.Tail + } else { + tail = "+1" + } + cmd := exec.Command("tail", "-n", tail, logPath) + stdout, err := cmd.CombinedOutput() + if err != nil { + return "", fmt.Errorf("tail -n %v failed, err: %v", req.Tail, err) + } + return string(stdout), nil +} + +func (c *ClamService) CleanRecord(req dto.OperateByID) error { + clam, _ := clamRepo.Get(commonRepo.WithByID(req.ID)) + if clam.ID == 0 { + return constant.ErrRecordNotFound + } + pathItem := path.Join(global.CONF.System.DataDir, resultDir, clam.Name) + _ = os.RemoveAll(pathItem) + return nil +} + +func (c *ClamService) LoadFile(req dto.ClamFileReq) (string, error) { + filePath := "" + switch req.Name { + case "clamd": + if c.serviceName == clamServiceNameUbuntu { + filePath = "/etc/clamav/clamd.conf" + } else { + filePath = "/etc/clamd.d/scan.conf" + } + case "clamd-log": + filePath = c.loadLogPath("clamd-log") + if len(filePath) != 0 { + break + } + if c.serviceName == clamServiceNameUbuntu { + filePath = "/var/log/clamav/clamav.log" + } else { + filePath = "/var/log/clamd.scan" + } + case "freshclam": + if c.serviceName == clamServiceNameUbuntu { + filePath = "/etc/clamav/freshclam.conf" + } else { + filePath = "/etc/freshclam.conf" + } + case "freshclam-log": + filePath = c.loadLogPath("freshclam-log") + if len(filePath) != 0 { + break + } + if c.serviceName == clamServiceNameUbuntu { + filePath = "/var/log/clamav/freshclam.log" + } else { + filePath = "/var/log/freshclam.log" + } + default: + return "", fmt.Errorf("not support such type") + } + if _, err := os.Stat(filePath); err != nil { + return "", buserr.New("ErrHttpReqNotFound") + } + var tail string + if req.Tail != "0" { + tail = req.Tail + } else { + tail = "+1" + } + cmd := exec.Command("tail", "-n", tail, filePath) + stdout, err := cmd.CombinedOutput() + if err != nil { + return "", fmt.Errorf("tail -n %v failed, err: %v", req.Tail, err) + } + return string(stdout), nil +} + +func (c *ClamService) UpdateFile(req dto.UpdateByNameAndFile) error { + filePath := "" + service := "" + switch req.Name { + case "clamd": + if c.serviceName == clamServiceNameUbuntu { + service = clamServiceNameUbuntu + filePath = "/etc/clamav/clamd.conf" + } else { + service = clamServiceNameCentOs + filePath = "/etc/clamd.d/scan.conf" + } + case "freshclam": + if c.serviceName == clamServiceNameUbuntu { + filePath = "/etc/clamav/freshclam.conf" + } else { + filePath = "/etc/freshclam.conf" + } + service = "clamav-freshclam.service" + default: + return fmt.Errorf("not support such type") + } + file, err := os.OpenFile(filePath, os.O_WRONLY|os.O_TRUNC, 0640) + if err != nil { + return err + } + defer file.Close() + write := bufio.NewWriter(file) + _, _ = write.WriteString(req.File) + write.Flush() + + _ = systemctl.Restart(service) + return nil +} + +func loadFileByName(name string) []string { + var logPaths []string + pathItem := path.Join(global.CONF.System.DataDir, resultDir, name) + _ = filepath.Walk(pathItem, func(path string, info os.FileInfo, err error) error { + if err != nil { + return nil + } + if info.IsDir() || info.Name() == name { + return nil + } + logPaths = append(logPaths, info.Name()) + return nil + }) + return logPaths +} +func loadResultFromLog(pathItem string) dto.ClamLog { + var data dto.ClamLog + data.Name = path.Base(pathItem) + data.Status = constant.StatusWaiting + file, err := os.ReadFile(pathItem) + if err != nil { + return data + } + lines := strings.Split(string(file), "\n") + for _, line := range lines { + if strings.Contains(line, "- SCAN SUMMARY -") { + data.Status = constant.StatusDone + } + if data.Status != constant.StatusDone { + continue + } + switch { + case strings.HasPrefix(line, "Infected files:"): + data.InfectedFiles = strings.TrimPrefix(line, "Infected files:") + case strings.HasPrefix(line, "Total errors:"): + data.TotalError = strings.TrimPrefix(line, "Total errors:") + case strings.HasPrefix(line, "Time:"): + if strings.Contains(line, "(") { + data.ScanTime = strings.ReplaceAll(strings.Split(line, "(")[1], ")", "") + continue + } + data.ScanTime = strings.TrimPrefix(line, "Time:") + case strings.HasPrefix(line, "Start Date:"): + data.ScanDate = strings.TrimPrefix(line, "Start Date:") + } + } + return data +} +func (c *ClamService) loadLogPath(name string) string { + confPath := "" + if name == "clamd-log" { + if c.serviceName == clamServiceNameUbuntu { + confPath = "/etc/clamav/clamd.conf" + } else { + confPath = "/etc/clamd.d/scan.conf" + } + } else { + if c.serviceName == clamServiceNameUbuntu { + confPath = "/etc/clamav/freshclam.conf" + } else { + confPath = "/etc/freshclam.conf" + } + } + if _, err := os.Stat(confPath); err != nil { + return "" + } + content, err := os.ReadFile(confPath) + if err != nil { + return "" + } + lines := strings.Split(string(content), "\n") + if name == "clamd-log" { + for _, line := range lines { + if strings.HasPrefix(line, "LogFile ") { + return strings.Trim(strings.ReplaceAll(line, "LogFile ", ""), " ") + } + } + } else { + for _, line := range lines { + if strings.HasPrefix(line, "UpdateLogFile ") { + return strings.Trim(strings.ReplaceAll(line, "UpdateLogFile ", ""), " ") + } + } + } + + return "" +} diff --git a/agent/app/service/command.go b/agent/app/service/command.go new file mode 100644 index 000000000..9cbca0777 --- /dev/null +++ b/agent/app/service/command.go @@ -0,0 +1,183 @@ +package service + +import ( + "github.com/1Panel-dev/1Panel/agent/app/dto" + "github.com/1Panel-dev/1Panel/agent/app/model" + "github.com/1Panel-dev/1Panel/agent/constant" + "github.com/jinzhu/copier" + "github.com/pkg/errors" +) + +type CommandService struct{} + +type ICommandService interface { + List() ([]dto.CommandInfo, error) + SearchForTree() ([]dto.CommandTree, error) + SearchWithPage(search dto.SearchCommandWithPage) (int64, interface{}, error) + Create(commandDto dto.CommandOperate) error + Update(id uint, upMap map[string]interface{}) error + Delete(ids []uint) error + + SearchRedisCommandWithPage(search dto.SearchWithPage) (int64, interface{}, error) + ListRedisCommand() ([]dto.RedisCommand, error) + SaveRedisCommand(commandDto dto.RedisCommand) error + DeleteRedisCommand(ids []uint) error +} + +func NewICommandService() ICommandService { + return &CommandService{} +} + +func (u *CommandService) List() ([]dto.CommandInfo, error) { + commands, err := commandRepo.GetList(commonRepo.WithOrderBy("name")) + if err != nil { + return nil, constant.ErrRecordNotFound + } + var dtoCommands []dto.CommandInfo + for _, command := range commands { + var item dto.CommandInfo + if err := copier.Copy(&item, &command); err != nil { + return nil, errors.WithMessage(constant.ErrStructTransform, err.Error()) + } + dtoCommands = append(dtoCommands, item) + } + return dtoCommands, err +} + +func (u *CommandService) SearchForTree() ([]dto.CommandTree, error) { + cmdList, err := commandRepo.GetList(commonRepo.WithOrderBy("name")) + if err != nil { + return nil, err + } + groups, err := groupRepo.GetList(commonRepo.WithByType("command")) + if err != nil { + return nil, err + } + var lists []dto.CommandTree + for _, group := range groups { + var data dto.CommandTree + data.ID = group.ID + 10000 + data.Label = group.Name + for _, cmd := range cmdList { + if cmd.GroupID == group.ID { + data.Children = append(data.Children, dto.CommandInfo{ID: cmd.ID, Name: cmd.Name, Command: cmd.Command}) + } + } + if len(data.Children) != 0 { + lists = append(lists, data) + } + } + return lists, err +} + +func (u *CommandService) SearchWithPage(search dto.SearchCommandWithPage) (int64, interface{}, error) { + total, commands, err := commandRepo.Page(search.Page, search.PageSize, commandRepo.WithLikeName(search.Name), commonRepo.WithLikeName(search.Info), commonRepo.WithByGroupID(search.GroupID), commonRepo.WithOrderRuleBy(search.OrderBy, search.Order)) + if err != nil { + return 0, nil, err + } + groups, _ := groupRepo.GetList(commonRepo.WithByType("command"), commonRepo.WithOrderBy("name")) + var dtoCommands []dto.CommandInfo + for _, command := range commands { + var item dto.CommandInfo + if err := copier.Copy(&item, &command); err != nil { + return 0, nil, errors.WithMessage(constant.ErrStructTransform, err.Error()) + } + for _, group := range groups { + if command.GroupID == group.ID { + item.GroupBelong = group.Name + item.GroupID = group.ID + } + } + dtoCommands = append(dtoCommands, item) + } + return total, dtoCommands, err +} + +func (u *CommandService) Create(commandDto dto.CommandOperate) error { + command, _ := commandRepo.Get(commonRepo.WithByName(commandDto.Name)) + if command.ID != 0 { + return constant.ErrRecordExist + } + if err := copier.Copy(&command, &commandDto); err != nil { + return errors.WithMessage(constant.ErrStructTransform, err.Error()) + } + if err := commandRepo.Create(&command); err != nil { + return err + } + return nil +} + +func (u *CommandService) Delete(ids []uint) error { + if len(ids) == 1 { + command, _ := commandRepo.Get(commonRepo.WithByID(ids[0])) + if command.ID == 0 { + return constant.ErrRecordNotFound + } + return commandRepo.Delete(commonRepo.WithByID(ids[0])) + } + return commandRepo.Delete(commonRepo.WithIdsIn(ids)) +} + +func (u *CommandService) Update(id uint, upMap map[string]interface{}) error { + return commandRepo.Update(id, upMap) +} + +func (u *CommandService) SearchRedisCommandWithPage(search dto.SearchWithPage) (int64, interface{}, error) { + total, commands, err := commandRepo.PageRedis(search.Page, search.PageSize, commandRepo.WithLikeName(search.Info)) + if err != nil { + return 0, nil, err + } + var dtoCommands []dto.RedisCommand + for _, command := range commands { + var item dto.RedisCommand + if err := copier.Copy(&item, &command); err != nil { + return 0, nil, errors.WithMessage(constant.ErrStructTransform, err.Error()) + } + dtoCommands = append(dtoCommands, item) + } + return total, dtoCommands, err +} + +func (u *CommandService) ListRedisCommand() ([]dto.RedisCommand, error) { + commands, err := commandRepo.GetRedisList() + if err != nil { + return nil, constant.ErrRecordNotFound + } + var dtoCommands []dto.RedisCommand + for _, command := range commands { + var item dto.RedisCommand + if err := copier.Copy(&item, &command); err != nil { + return nil, errors.WithMessage(constant.ErrStructTransform, err.Error()) + } + dtoCommands = append(dtoCommands, item) + } + return dtoCommands, err +} + +func (u *CommandService) SaveRedisCommand(req dto.RedisCommand) error { + if req.ID == 0 { + command, _ := commandRepo.GetRedis(commonRepo.WithByName(req.Name)) + if command.ID != 0 { + return constant.ErrRecordExist + } + } + var command model.RedisCommand + if err := copier.Copy(&command, &req); err != nil { + return errors.WithMessage(constant.ErrStructTransform, err.Error()) + } + if err := commandRepo.SaveRedis(&command); err != nil { + return err + } + return nil +} + +func (u *CommandService) DeleteRedisCommand(ids []uint) error { + if len(ids) == 1 { + command, _ := commandRepo.GetRedis(commonRepo.WithByID(ids[0])) + if command.ID == 0 { + return constant.ErrRecordNotFound + } + return commandRepo.DeleteRedis(commonRepo.WithByID(ids[0])) + } + return commandRepo.DeleteRedis(commonRepo.WithIdsIn(ids)) +} diff --git a/agent/app/service/compose_template.go b/agent/app/service/compose_template.go new file mode 100644 index 000000000..dd4f91eca --- /dev/null +++ b/agent/app/service/compose_template.go @@ -0,0 +1,80 @@ +package service + +import ( + "github.com/1Panel-dev/1Panel/agent/app/dto" + "github.com/1Panel-dev/1Panel/agent/constant" + "github.com/jinzhu/copier" + "github.com/pkg/errors" +) + +type ComposeTemplateService struct{} + +type IComposeTemplateService interface { + List() ([]dto.ComposeTemplateInfo, error) + SearchWithPage(search dto.SearchWithPage) (int64, interface{}, error) + Create(composeDto dto.ComposeTemplateCreate) error + Update(id uint, upMap map[string]interface{}) error + Delete(ids []uint) error +} + +func NewIComposeTemplateService() IComposeTemplateService { + return &ComposeTemplateService{} +} + +func (u *ComposeTemplateService) List() ([]dto.ComposeTemplateInfo, error) { + composes, err := composeRepo.List() + if err != nil { + return nil, constant.ErrRecordNotFound + } + var dtoLists []dto.ComposeTemplateInfo + for _, compose := range composes { + var item dto.ComposeTemplateInfo + if err := copier.Copy(&item, &compose); err != nil { + return nil, errors.WithMessage(constant.ErrStructTransform, err.Error()) + } + dtoLists = append(dtoLists, item) + } + return dtoLists, err +} + +func (u *ComposeTemplateService) SearchWithPage(req dto.SearchWithPage) (int64, interface{}, error) { + total, composes, err := composeRepo.Page(req.Page, req.PageSize, commonRepo.WithLikeName(req.Info)) + var dtoComposeTemplates []dto.ComposeTemplateInfo + for _, compose := range composes { + var item dto.ComposeTemplateInfo + if err := copier.Copy(&item, &compose); err != nil { + return 0, nil, errors.WithMessage(constant.ErrStructTransform, err.Error()) + } + dtoComposeTemplates = append(dtoComposeTemplates, item) + } + return total, dtoComposeTemplates, err +} + +func (u *ComposeTemplateService) Create(composeDto dto.ComposeTemplateCreate) error { + compose, _ := composeRepo.Get(commonRepo.WithByName(composeDto.Name)) + if compose.ID != 0 { + return constant.ErrRecordExist + } + if err := copier.Copy(&compose, &composeDto); err != nil { + return errors.WithMessage(constant.ErrStructTransform, err.Error()) + } + if err := composeRepo.Create(&compose); err != nil { + return err + } + return nil +} + +func (u *ComposeTemplateService) Delete(ids []uint) error { + if len(ids) == 1 { + compose, _ := composeRepo.Get(commonRepo.WithByID(ids[0])) + if compose.ID == 0 { + return constant.ErrRecordNotFound + } + return composeRepo.Delete(commonRepo.WithByID(ids[0])) + } + return composeRepo.Delete(commonRepo.WithIdsIn(ids)) +} + +func (u *ComposeTemplateService) Update(id uint, upMap map[string]interface{}) error { + return composeRepo.Update(id, upMap) +} diff --git a/agent/app/service/container.go b/agent/app/service/container.go new file mode 100644 index 000000000..4f6004da4 --- /dev/null +++ b/agent/app/service/container.go @@ -0,0 +1,1312 @@ +package service + +import ( + "bufio" + "context" + "encoding/base64" + "encoding/json" + "fmt" + "io" + "net/http" + "net/url" + "os" + "os/exec" + "path/filepath" + "sort" + "strconv" + "strings" + "sync" + "syscall" + "time" + "unicode/utf8" + + "github.com/gin-gonic/gin" + + "github.com/pkg/errors" + + "github.com/1Panel-dev/1Panel/agent/app/dto" + "github.com/1Panel-dev/1Panel/agent/buserr" + "github.com/1Panel-dev/1Panel/agent/constant" + "github.com/1Panel-dev/1Panel/agent/global" + "github.com/1Panel-dev/1Panel/agent/utils/cmd" + "github.com/1Panel-dev/1Panel/agent/utils/common" + "github.com/1Panel-dev/1Panel/agent/utils/docker" + "github.com/docker/docker/api/types" + "github.com/docker/docker/api/types/container" + "github.com/docker/docker/api/types/filters" + "github.com/docker/docker/api/types/image" + "github.com/docker/docker/api/types/mount" + "github.com/docker/docker/api/types/network" + "github.com/docker/docker/api/types/registry" + "github.com/docker/docker/client" + "github.com/docker/go-connections/nat" + "github.com/gorilla/websocket" + v1 "github.com/opencontainers/image-spec/specs-go/v1" + "github.com/shirou/gopsutil/v3/cpu" + "github.com/shirou/gopsutil/v3/mem" +) + +type ContainerService struct{} + +type IContainerService interface { + Page(req dto.PageContainer) (int64, interface{}, error) + List() ([]string, error) + PageNetwork(req dto.SearchWithPage) (int64, interface{}, error) + ListNetwork() ([]dto.Options, error) + PageVolume(req dto.SearchWithPage) (int64, interface{}, error) + ListVolume() ([]dto.Options, error) + PageCompose(req dto.SearchWithPage) (int64, interface{}, error) + CreateCompose(req dto.ComposeCreate) (string, error) + ComposeOperation(req dto.ComposeOperation) error + ContainerCreate(req dto.ContainerOperate) error + ContainerUpdate(req dto.ContainerOperate) error + ContainerUpgrade(req dto.ContainerUpgrade) error + ContainerInfo(req dto.OperationWithName) (*dto.ContainerOperate, error) + ContainerListStats() ([]dto.ContainerListStats, error) + LoadResourceLimit() (*dto.ResourceLimit, error) + ContainerRename(req dto.ContainerRename) error + ContainerCommit(req dto.ContainerCommit) error + ContainerLogClean(req dto.OperationWithName) error + ContainerOperation(req dto.ContainerOperation) error + ContainerLogs(wsConn *websocket.Conn, containerType, container, since, tail string, follow bool) error + DownloadContainerLogs(containerType, container, since, tail string, c *gin.Context) error + ContainerStats(id string) (*dto.ContainerStats, error) + Inspect(req dto.InspectReq) (string, error) + DeleteNetwork(req dto.BatchDelete) error + CreateNetwork(req dto.NetworkCreate) error + DeleteVolume(req dto.BatchDelete) error + CreateVolume(req dto.VolumeCreate) error + TestCompose(req dto.ComposeCreate) (bool, error) + ComposeUpdate(req dto.ComposeUpdate) error + Prune(req dto.ContainerPrune) (dto.ContainerPruneReport, error) + + LoadContainerLogs(req dto.OperationWithNameAndType) string +} + +func NewIContainerService() IContainerService { + return &ContainerService{} +} + +func (u *ContainerService) Page(req dto.PageContainer) (int64, interface{}, error) { + var ( + records []types.Container + list []types.Container + ) + client, err := docker.NewDockerClient() + if err != nil { + return 0, nil, err + } + defer client.Close() + options := container.ListOptions{ + All: true, + } + if len(req.Filters) != 0 { + options.Filters = filters.NewArgs() + options.Filters.Add("label", req.Filters) + } + containers, err := client.ContainerList(context.Background(), options) + if err != nil { + return 0, nil, err + } + if req.ExcludeAppStore { + for _, item := range containers { + if created, ok := item.Labels[composeCreatedBy]; ok && created == "Apps" { + continue + } + list = append(list, item) + } + } else { + list = containers + } + + if len(req.Name) != 0 { + length, count := len(list), 0 + for count < length { + if !strings.Contains(list[count].Names[0][1:], req.Name) { + list = append(list[:count], list[(count+1):]...) + length-- + } else { + count++ + } + } + } + if req.State != "all" { + length, count := len(list), 0 + for count < length { + if list[count].State != req.State { + list = append(list[:count], list[(count+1):]...) + length-- + } else { + count++ + } + } + } + switch req.OrderBy { + case "name": + sort.Slice(list, func(i, j int) bool { + if req.Order == constant.OrderAsc { + return list[i].Names[0][1:] < list[j].Names[0][1:] + } + return list[i].Names[0][1:] > list[j].Names[0][1:] + }) + case "state": + sort.Slice(list, func(i, j int) bool { + if req.Order == constant.OrderAsc { + return list[i].State < list[j].State + } + return list[i].State > list[j].State + }) + default: + sort.Slice(list, func(i, j int) bool { + if req.Order == constant.OrderAsc { + return list[i].Created < list[j].Created + } + return list[i].Created > list[j].Created + }) + } + + total, start, end := len(list), (req.Page-1)*req.PageSize, req.Page*req.PageSize + if start > total { + records = make([]types.Container, 0) + } else { + if end >= total { + end = total + } + records = list[start:end] + } + + backDatas := make([]dto.ContainerInfo, len(records)) + for i := 0; i < len(records); i++ { + item := records[i] + IsFromCompose := false + if _, ok := item.Labels[composeProjectLabel]; ok { + IsFromCompose = true + } + IsFromApp := false + if created, ok := item.Labels[composeCreatedBy]; ok && created == "Apps" { + IsFromApp = true + } + + ports := loadContainerPort(item.Ports) + info := dto.ContainerInfo{ + ContainerID: item.ID, + CreateTime: time.Unix(item.Created, 0).Format(constant.DateTimeLayout), + Name: item.Names[0][1:], + ImageId: strings.Split(item.ImageID, ":")[1], + ImageName: item.Image, + State: item.State, + RunTime: item.Status, + Ports: ports, + IsFromApp: IsFromApp, + IsFromCompose: IsFromCompose, + } + install, _ := appInstallRepo.GetFirst(appInstallRepo.WithContainerName(info.Name)) + if install.ID > 0 { + info.AppInstallName = install.Name + info.AppName = install.App.Name + websites, _ := websiteRepo.GetBy(websiteRepo.WithAppInstallId(install.ID)) + for _, website := range websites { + info.Websites = append(info.Websites, website.PrimaryDomain) + } + } + backDatas[i] = info + if item.NetworkSettings != nil && len(item.NetworkSettings.Networks) > 0 { + networks := make([]string, 0, len(item.NetworkSettings.Networks)) + for key := range item.NetworkSettings.Networks { + networks = append(networks, item.NetworkSettings.Networks[key].IPAddress) + } + sort.Strings(networks) + backDatas[i].Network = networks + } + } + + return int64(total), backDatas, nil +} + +func (u *ContainerService) List() ([]string, error) { + client, err := docker.NewDockerClient() + if err != nil { + return nil, err + } + defer client.Close() + containers, err := client.ContainerList(context.Background(), container.ListOptions{All: true}) + if err != nil { + return nil, err + } + var datas []string + for _, container := range containers { + for _, name := range container.Names { + if len(name) != 0 { + datas = append(datas, strings.TrimPrefix(name, "/")) + } + } + } + + return datas, nil +} + +func (u *ContainerService) ContainerListStats() ([]dto.ContainerListStats, error) { + client, err := docker.NewDockerClient() + if err != nil { + return nil, err + } + defer client.Close() + list, err := client.ContainerList(context.Background(), container.ListOptions{All: true}) + if err != nil { + return nil, err + } + var datas []dto.ContainerListStats + var wg sync.WaitGroup + wg.Add(len(list)) + for i := 0; i < len(list); i++ { + go func(item types.Container) { + datas = append(datas, loadCpuAndMem(client, item.ID)) + wg.Done() + }(list[i]) + } + wg.Wait() + return datas, nil +} + +func (u *ContainerService) Inspect(req dto.InspectReq) (string, error) { + client, err := docker.NewDockerClient() + if err != nil { + return "", err + } + defer client.Close() + var inspectInfo interface{} + switch req.Type { + case "container": + inspectInfo, err = client.ContainerInspect(context.Background(), req.ID) + case "image": + inspectInfo, _, err = client.ImageInspectWithRaw(context.Background(), req.ID) + case "network": + inspectInfo, err = client.NetworkInspect(context.TODO(), req.ID, types.NetworkInspectOptions{}) + case "volume": + inspectInfo, err = client.VolumeInspect(context.TODO(), req.ID) + } + if err != nil { + return "", err + } + bytes, err := json.Marshal(inspectInfo) + if err != nil { + return "", err + } + return string(bytes), nil +} + +func (u *ContainerService) Prune(req dto.ContainerPrune) (dto.ContainerPruneReport, error) { + report := dto.ContainerPruneReport{} + client, err := docker.NewDockerClient() + if err != nil { + return report, err + } + defer client.Close() + pruneFilters := filters.NewArgs() + if req.WithTagAll { + pruneFilters.Add("dangling", "false") + if req.PruneType != "image" { + pruneFilters.Add("until", "24h") + } + } + switch req.PruneType { + case "container": + rep, err := client.ContainersPrune(context.Background(), pruneFilters) + if err != nil { + return report, err + } + report.DeletedNumber = len(rep.ContainersDeleted) + report.SpaceReclaimed = int(rep.SpaceReclaimed) + case "image": + rep, err := client.ImagesPrune(context.Background(), pruneFilters) + if err != nil { + return report, err + } + report.DeletedNumber = len(rep.ImagesDeleted) + report.SpaceReclaimed = int(rep.SpaceReclaimed) + case "network": + rep, err := client.NetworksPrune(context.Background(), pruneFilters) + if err != nil { + return report, err + } + report.DeletedNumber = len(rep.NetworksDeleted) + case "volume": + versions, err := client.ServerVersion(context.Background()) + if err != nil { + return report, err + } + if common.ComparePanelVersion(versions.APIVersion, "1.42") { + pruneFilters.Add("all", "true") + } + rep, err := client.VolumesPrune(context.Background(), pruneFilters) + if err != nil { + return report, err + } + report.DeletedNumber = len(rep.VolumesDeleted) + report.SpaceReclaimed = int(rep.SpaceReclaimed) + case "buildcache": + opts := types.BuildCachePruneOptions{} + opts.All = true + rep, err := client.BuildCachePrune(context.Background(), opts) + if err != nil { + return report, err + } + report.DeletedNumber = len(rep.CachesDeleted) + report.SpaceReclaimed = int(rep.SpaceReclaimed) + } + return report, nil +} + +func (u *ContainerService) LoadResourceLimit() (*dto.ResourceLimit, error) { + cpuCounts, err := cpu.Counts(true) + if err != nil { + return nil, fmt.Errorf("load cpu limit failed, err: %v", err) + } + memoryInfo, err := mem.VirtualMemory() + if err != nil { + return nil, fmt.Errorf("load memory limit failed, err: %v", err) + } + + data := dto.ResourceLimit{ + CPU: cpuCounts, + Memory: memoryInfo.Total, + } + return &data, nil +} + +func (u *ContainerService) ContainerCreate(req dto.ContainerOperate) error { + client, err := docker.NewDockerClient() + if err != nil { + return err + } + defer client.Close() + ctx := context.Background() + newContainer, _ := client.ContainerInspect(ctx, req.Name) + if newContainer.ContainerJSONBase != nil { + return buserr.New(constant.ErrContainerName) + } + + if !checkImageExist(client, req.Image) || req.ForcePull { + if err := pullImages(ctx, client, req.Image); err != nil { + if !req.ForcePull { + return err + } + global.LOG.Errorf("force pull image %s failed, err: %v", req.Image, err) + } + } + imageInfo, _, err := client.ImageInspectWithRaw(ctx, req.Image) + if err != nil { + return err + } + if len(req.Entrypoint) == 0 { + req.Entrypoint = imageInfo.Config.Entrypoint + } + if len(req.Cmd) == 0 { + req.Cmd = imageInfo.Config.Cmd + } + config, hostConf, networkConf, err := loadConfigInfo(true, req, nil) + if err != nil { + return err + } + global.LOG.Infof("new container info %s has been made, now start to create", req.Name) + con, err := client.ContainerCreate(ctx, config, hostConf, networkConf, &v1.Platform{}, req.Name) + if err != nil { + _ = client.ContainerRemove(ctx, req.Name, container.RemoveOptions{RemoveVolumes: true, Force: true}) + return err + } + global.LOG.Infof("create container %s successful! now check if the container is started and delete the container information if it is not.", req.Name) + if err := client.ContainerStart(ctx, con.ID, container.StartOptions{}); err != nil { + _ = client.ContainerRemove(ctx, req.Name, container.RemoveOptions{RemoveVolumes: true, Force: true}) + return fmt.Errorf("create successful but start failed, err: %v", err) + } + return nil +} + +func (u *ContainerService) ContainerInfo(req dto.OperationWithName) (*dto.ContainerOperate, error) { + client, err := docker.NewDockerClient() + if err != nil { + return nil, err + } + defer client.Close() + ctx := context.Background() + oldContainer, err := client.ContainerInspect(ctx, req.Name) + if err != nil { + return nil, err + } + + var data dto.ContainerOperate + data.ContainerID = oldContainer.ID + data.Name = strings.ReplaceAll(oldContainer.Name, "/", "") + data.Image = oldContainer.Config.Image + if oldContainer.NetworkSettings != nil { + for network := range oldContainer.NetworkSettings.Networks { + data.Network = network + break + } + } + + networkSettings := oldContainer.NetworkSettings + bridgeNetworkSettings := networkSettings.Networks[data.Network] + if bridgeNetworkSettings.IPAMConfig != nil { + ipv4Address := bridgeNetworkSettings.IPAMConfig.IPv4Address + data.Ipv4 = ipv4Address + ipv6Address := bridgeNetworkSettings.IPAMConfig.IPv6Address + data.Ipv6 = ipv6Address + } else { + data.Ipv4 = bridgeNetworkSettings.IPAddress + } + + data.Cmd = oldContainer.Config.Cmd + data.OpenStdin = oldContainer.Config.OpenStdin + data.Tty = oldContainer.Config.Tty + data.Entrypoint = oldContainer.Config.Entrypoint + data.Env = oldContainer.Config.Env + data.CPUShares = oldContainer.HostConfig.CPUShares + for key, val := range oldContainer.Config.Labels { + data.Labels = append(data.Labels, fmt.Sprintf("%s=%s", key, val)) + } + for key, val := range oldContainer.HostConfig.PortBindings { + var itemPort dto.PortHelper + if !strings.Contains(string(key), "/") { + continue + } + itemPort.ContainerPort = strings.Split(string(key), "/")[0] + itemPort.Protocol = strings.Split(string(key), "/")[1] + for _, binds := range val { + itemPort.HostIP = binds.HostIP + itemPort.HostPort = binds.HostPort + data.ExposedPorts = append(data.ExposedPorts, itemPort) + } + } + data.AutoRemove = oldContainer.HostConfig.AutoRemove + data.Privileged = oldContainer.HostConfig.Privileged + data.PublishAllPorts = oldContainer.HostConfig.PublishAllPorts + data.RestartPolicy = string(oldContainer.HostConfig.RestartPolicy.Name) + if oldContainer.HostConfig.NanoCPUs != 0 { + data.NanoCPUs = float64(oldContainer.HostConfig.NanoCPUs) / 1000000000 + } + if oldContainer.HostConfig.Memory != 0 { + data.Memory = float64(oldContainer.HostConfig.Memory) / 1024 / 1024 + } + data.Volumes = loadVolumeBinds(oldContainer.Mounts) + + return &data, nil +} + +func (u *ContainerService) ContainerUpdate(req dto.ContainerOperate) error { + client, err := docker.NewDockerClient() + if err != nil { + return err + } + defer client.Close() + ctx := context.Background() + newContainer, _ := client.ContainerInspect(ctx, req.Name) + if newContainer.ContainerJSONBase != nil && newContainer.ID != req.ContainerID { + return buserr.New(constant.ErrContainerName) + } + + oldContainer, err := client.ContainerInspect(ctx, req.ContainerID) + if err != nil { + return err + } + if !checkImageExist(client, req.Image) || req.ForcePull { + if err := pullImages(ctx, client, req.Image); err != nil { + if !req.ForcePull { + return err + } + return fmt.Errorf("pull image %s failed, err: %v", req.Image, err) + } + } + + if err := client.ContainerRemove(ctx, req.ContainerID, container.RemoveOptions{Force: true}); err != nil { + return err + } + + config, hostConf, networkConf, err := loadConfigInfo(false, req, &oldContainer) + if err != nil { + reCreateAfterUpdate(req.Name, client, oldContainer.Config, oldContainer.HostConfig, oldContainer.NetworkSettings) + return err + } + + global.LOG.Infof("new container info %s has been update, now start to recreate", req.Name) + con, err := client.ContainerCreate(ctx, config, hostConf, networkConf, &v1.Platform{}, req.Name) + if err != nil { + reCreateAfterUpdate(req.Name, client, oldContainer.Config, oldContainer.HostConfig, oldContainer.NetworkSettings) + return fmt.Errorf("update container failed, err: %v", err) + } + global.LOG.Infof("update container %s successful! now check if the container is started.", req.Name) + if err := client.ContainerStart(ctx, con.ID, container.StartOptions{}); err != nil { + return fmt.Errorf("update successful but start failed, err: %v", err) + } + + return nil +} + +func (u *ContainerService) ContainerUpgrade(req dto.ContainerUpgrade) error { + client, err := docker.NewDockerClient() + if err != nil { + return err + } + defer client.Close() + ctx := context.Background() + oldContainer, err := client.ContainerInspect(ctx, req.Name) + if err != nil { + return err + } + if !checkImageExist(client, req.Image) || req.ForcePull { + if err := pullImages(ctx, client, req.Image); err != nil { + if !req.ForcePull { + return err + } + return fmt.Errorf("pull image %s failed, err: %v", req.Image, err) + } + } + config := oldContainer.Config + config.Image = req.Image + hostConf := oldContainer.HostConfig + var networkConf network.NetworkingConfig + if oldContainer.NetworkSettings != nil { + for networkKey := range oldContainer.NetworkSettings.Networks { + networkConf.EndpointsConfig = map[string]*network.EndpointSettings{networkKey: {}} + break + } + } + if err := client.ContainerRemove(ctx, req.Name, container.RemoveOptions{Force: true}); err != nil { + return err + } + + global.LOG.Infof("new container info %s has been update, now start to recreate", req.Name) + con, err := client.ContainerCreate(ctx, config, hostConf, &networkConf, &v1.Platform{}, req.Name) + if err != nil { + reCreateAfterUpdate(req.Name, client, oldContainer.Config, oldContainer.HostConfig, oldContainer.NetworkSettings) + return fmt.Errorf("upgrade container failed, err: %v", err) + } + global.LOG.Infof("upgrade container %s successful! now check if the container is started.", req.Name) + if err := client.ContainerStart(ctx, con.ID, container.StartOptions{}); err != nil { + return fmt.Errorf("upgrade successful but start failed, err: %v", err) + } + + return nil +} + +func (u *ContainerService) ContainerRename(req dto.ContainerRename) error { + ctx := context.Background() + client, err := docker.NewDockerClient() + if err != nil { + return err + } + defer client.Close() + + newContainer, _ := client.ContainerInspect(ctx, req.NewName) + if newContainer.ContainerJSONBase != nil { + return buserr.New(constant.ErrContainerName) + } + return client.ContainerRename(ctx, req.Name, req.NewName) +} + +func (u *ContainerService) ContainerCommit(req dto.ContainerCommit) error { + ctx := context.Background() + client, err := docker.NewDockerClient() + if err != nil { + return err + } + defer client.Close() + options := container.CommitOptions{ + Reference: req.NewImageName, + Comment: req.Comment, + Author: req.Author, + Changes: nil, + Pause: req.Pause, + Config: nil, + } + _, err = client.ContainerCommit(ctx, req.ContainerId, options) + if err != nil { + return fmt.Errorf("failed to commit container, err: %v", err) + } + return nil +} + +func (u *ContainerService) ContainerOperation(req dto.ContainerOperation) error { + var err error + ctx := context.Background() + client, err := docker.NewDockerClient() + if err != nil { + return err + } + defer client.Close() + for _, item := range req.Names { + global.LOG.Infof("start container %s operation %s", item, req.Operation) + switch req.Operation { + case constant.ContainerOpStart: + err = client.ContainerStart(ctx, item, container.StartOptions{}) + case constant.ContainerOpStop: + err = client.ContainerStop(ctx, item, container.StopOptions{}) + case constant.ContainerOpRestart: + err = client.ContainerRestart(ctx, item, container.StopOptions{}) + case constant.ContainerOpKill: + err = client.ContainerKill(ctx, item, "SIGKILL") + case constant.ContainerOpPause: + err = client.ContainerPause(ctx, item) + case constant.ContainerOpUnpause: + err = client.ContainerUnpause(ctx, item) + case constant.ContainerOpRemove: + err = client.ContainerRemove(ctx, item, container.RemoveOptions{RemoveVolumes: true, Force: true}) + } + } + return err +} + +func (u *ContainerService) ContainerLogClean(req dto.OperationWithName) error { + client, err := docker.NewDockerClient() + if err != nil { + return err + } + defer client.Close() + ctx := context.Background() + containerItem, err := client.ContainerInspect(ctx, req.Name) + if err != nil { + return err + } + if err := client.ContainerStop(ctx, containerItem.ID, container.StopOptions{}); err != nil { + return err + } + file, err := os.OpenFile(containerItem.LogPath, os.O_RDWR|os.O_CREATE, 0666) + if err != nil { + return err + } + defer file.Close() + if err = file.Truncate(0); err != nil { + return err + } + _, _ = file.Seek(0, 0) + + files, _ := filepath.Glob(fmt.Sprintf("%s.*", containerItem.LogPath)) + for _, file := range files { + _ = os.Remove(file) + } + + if err := client.ContainerStart(ctx, containerItem.ID, container.StartOptions{}); err != nil { + return err + } + return nil +} + +func (u *ContainerService) ContainerLogs(wsConn *websocket.Conn, containerType, container, since, tail string, follow bool) error { + defer func() { wsConn.Close() }() + if cmd.CheckIllegal(container, since, tail) { + return buserr.New(constant.ErrCmdIllegal) + } + commandName := "docker" + commandArg := []string{"logs", container} + if containerType == "compose" { + commandName = "docker-compose" + commandArg = []string{"-f", container, "logs"} + } + if tail != "0" { + commandArg = append(commandArg, "--tail") + commandArg = append(commandArg, tail) + } + if since != "all" { + commandArg = append(commandArg, "--since") + commandArg = append(commandArg, since) + } + if follow { + commandArg = append(commandArg, "-f") + } + if !follow { + cmd := exec.Command(commandName, commandArg...) + cmd.Stderr = cmd.Stdout + stdout, _ := cmd.CombinedOutput() + if !utf8.Valid(stdout) { + return errors.New("invalid utf8") + } + if err := wsConn.WriteMessage(websocket.TextMessage, stdout); err != nil { + global.LOG.Errorf("send message with log to ws failed, err: %v", err) + } + return nil + } + + cmd := exec.Command(commandName, commandArg...) + stdout, err := cmd.StdoutPipe() + if err != nil { + _ = cmd.Process.Signal(syscall.SIGTERM) + return err + } + cmd.Stderr = cmd.Stdout + if err := cmd.Start(); err != nil { + _ = cmd.Process.Signal(syscall.SIGTERM) + return err + } + exitCh := make(chan struct{}) + go func() { + _, wsData, _ := wsConn.ReadMessage() + if string(wsData) == "close conn" { + _ = cmd.Process.Signal(syscall.SIGTERM) + exitCh <- struct{}{} + } + }() + + go func() { + buffer := make([]byte, 1024) + for { + select { + case <-exitCh: + return + default: + n, err := stdout.Read(buffer) + if err != nil { + if err == io.EOF { + return + } + global.LOG.Errorf("read bytes from log failed, err: %v", err) + return + } + if !utf8.Valid(buffer[:n]) { + continue + } + if err = wsConn.WriteMessage(websocket.TextMessage, buffer[:n]); err != nil { + global.LOG.Errorf("send message with log to ws failed, err: %v", err) + return + } + } + } + }() + _ = cmd.Wait() + return nil +} + +func (u *ContainerService) DownloadContainerLogs(containerType, container, since, tail string, c *gin.Context) error { + if cmd.CheckIllegal(container, since, tail) { + return buserr.New(constant.ErrCmdIllegal) + } + commandName := "docker" + commandArg := []string{"logs", container} + if containerType == "compose" { + commandName = "docker-compose" + commandArg = []string{"-f", container, "logs"} + } + if tail != "0" { + commandArg = append(commandArg, "--tail") + commandArg = append(commandArg, tail) + } + if since != "all" { + commandArg = append(commandArg, "--since") + commandArg = append(commandArg, since) + } + + cmd := exec.Command(commandName, commandArg...) + stdout, err := cmd.StdoutPipe() + if err != nil { + _ = cmd.Process.Signal(syscall.SIGTERM) + return err + } + cmd.Stderr = cmd.Stdout + if err := cmd.Start(); err != nil { + _ = cmd.Process.Signal(syscall.SIGTERM) + return err + } + + tempFile, err := os.CreateTemp("", "cmd_output_*.txt") + if err != nil { + return err + } + defer tempFile.Close() + defer func() { + if err := os.Remove(tempFile.Name()); err != nil { + global.LOG.Errorf("os.Remove() failed: %v", err) + } + }() + errCh := make(chan error) + go func() { + scanner := bufio.NewScanner(stdout) + for scanner.Scan() { + line := scanner.Text() + if _, err := tempFile.WriteString(line + "\n"); err != nil { + errCh <- err + return + } + } + if err := scanner.Err(); err != nil { + errCh <- err + return + } + errCh <- nil + }() + select { + case err := <-errCh: + if err != nil { + global.LOG.Errorf("Error: %v", err) + } + case <-time.After(3 * time.Second): + global.LOG.Errorf("Timeout reached") + } + info, _ := tempFile.Stat() + + c.Header("Content-Length", strconv.FormatInt(info.Size(), 10)) + c.Header("Content-Disposition", "attachment; filename*=utf-8''"+url.PathEscape(info.Name())) + http.ServeContent(c.Writer, c.Request, info.Name(), info.ModTime(), tempFile) + return nil +} + +func (u *ContainerService) ContainerStats(id string) (*dto.ContainerStats, error) { + client, err := docker.NewDockerClient() + if err != nil { + return nil, err + } + defer client.Close() + res, err := client.ContainerStats(context.TODO(), id, false) + if err != nil { + return nil, err + } + defer res.Body.Close() + + body, err := io.ReadAll(res.Body) + if err != nil { + res.Body.Close() + return nil, err + } + res.Body.Close() + var stats *types.StatsJSON + if err := json.Unmarshal(body, &stats); err != nil { + return nil, err + } + var data dto.ContainerStats + data.CPUPercent = calculateCPUPercentUnix(stats) + data.IORead, data.IOWrite = calculateBlockIO(stats.BlkioStats) + data.Memory = float64(stats.MemoryStats.Usage) / 1024 / 1024 + if cache, ok := stats.MemoryStats.Stats["cache"]; ok { + data.Cache = float64(cache) / 1024 / 1024 + } + data.NetworkRX, data.NetworkTX = calculateNetwork(stats.Networks) + data.ShotTime = stats.Read + return &data, nil +} + +func (u *ContainerService) LoadContainerLogs(req dto.OperationWithNameAndType) string { + filePath := "" + if req.Type == "compose-detail" { + cli, err := docker.NewDockerClient() + if err != nil { + return "" + } + defer cli.Close() + options := container.ListOptions{All: true} + options.Filters = filters.NewArgs() + options.Filters.Add("label", fmt.Sprintf("%s=%s", composeProjectLabel, req.Name)) + containers, err := cli.ContainerList(context.Background(), options) + if err != nil { + return "" + } + for _, container := range containers { + config := container.Labels[composeConfigLabel] + workdir := container.Labels[composeWorkdirLabel] + if len(config) != 0 && len(workdir) != 0 && strings.Contains(config, workdir) { + filePath = config + break + } else { + filePath = workdir + break + } + } + } + if _, err := os.Stat(filePath); err != nil { + return "" + } + content, err := os.ReadFile(filePath) + if err != nil { + return "" + } + return string(content) +} + +func stringsToMap(list []string) map[string]string { + var labelMap = make(map[string]string) + for _, label := range list { + if strings.Contains(label, "=") { + sps := strings.SplitN(label, "=", 2) + labelMap[sps[0]] = sps[1] + } + } + return labelMap +} + +func calculateCPUPercentUnix(stats *types.StatsJSON) float64 { + cpuPercent := 0.0 + cpuDelta := float64(stats.CPUStats.CPUUsage.TotalUsage) - float64(stats.PreCPUStats.CPUUsage.TotalUsage) + systemDelta := float64(stats.CPUStats.SystemUsage) - float64(stats.PreCPUStats.SystemUsage) + + if systemDelta > 0.0 && cpuDelta > 0.0 { + cpuPercent = (cpuDelta / systemDelta) * 100.0 + if len(stats.CPUStats.CPUUsage.PercpuUsage) != 0 { + cpuPercent = cpuPercent * float64(len(stats.CPUStats.CPUUsage.PercpuUsage)) + } + } + return cpuPercent +} +func calculateMemPercentUnix(memStats types.MemoryStats) float64 { + memPercent := 0.0 + memUsage := float64(memStats.Usage) + memLimit := float64(memStats.Limit) + if memUsage > 0.0 && memLimit > 0.0 { + memPercent = (memUsage / memLimit) * 100.0 + } + return memPercent +} +func calculateBlockIO(blkio types.BlkioStats) (blkRead float64, blkWrite float64) { + for _, bioEntry := range blkio.IoServiceBytesRecursive { + switch strings.ToLower(bioEntry.Op) { + case "read": + blkRead = (blkRead + float64(bioEntry.Value)) / 1024 / 1024 + case "write": + blkWrite = (blkWrite + float64(bioEntry.Value)) / 1024 / 1024 + } + } + return +} +func calculateNetwork(network map[string]types.NetworkStats) (float64, float64) { + var rx, tx float64 + + for _, v := range network { + rx += float64(v.RxBytes) / 1024 + tx += float64(v.TxBytes) / 1024 + } + return rx, tx +} + +func checkImageExist(client *client.Client, imageItem string) bool { + images, err := client.ImageList(context.Background(), image.ListOptions{}) + if err != nil { + return false + } + + for _, img := range images { + for _, tag := range img.RepoTags { + if tag == imageItem || tag == imageItem+":latest" { + return true + } + } + } + return false +} + +func pullImages(ctx context.Context, client *client.Client, imageName string) error { + options := image.PullOptions{} + repos, _ := imageRepoRepo.List() + if len(repos) != 0 { + for _, repo := range repos { + if strings.HasPrefix(imageName, repo.DownloadUrl) && repo.Auth { + authConfig := registry.AuthConfig{ + Username: repo.Username, + Password: repo.Password, + } + encodedJSON, err := json.Marshal(authConfig) + if err != nil { + return err + } + authStr := base64.URLEncoding.EncodeToString(encodedJSON) + options.RegistryAuth = authStr + } + } + } else { + hasAuth, authStr := loadAuthInfo(imageName) + if hasAuth { + options.RegistryAuth = authStr + } + } + out, err := client.ImagePull(ctx, imageName, options) + if err != nil { + return err + } + defer out.Close() + _, err = io.Copy(io.Discard, out) + if err != nil { + return err + } + return nil +} + +func loadCpuAndMem(client *client.Client, container string) dto.ContainerListStats { + data := dto.ContainerListStats{ + ContainerID: container, + } + res, err := client.ContainerStats(context.Background(), container, false) + if err != nil { + return data + } + + defer res.Body.Close() + body, err := io.ReadAll(res.Body) + if err != nil { + return data + } + var stats *types.StatsJSON + if err := json.Unmarshal(body, &stats); err != nil { + return data + } + + data.CPUTotalUsage = stats.CPUStats.CPUUsage.TotalUsage - stats.PreCPUStats.CPUUsage.TotalUsage + data.SystemUsage = stats.CPUStats.SystemUsage - stats.PreCPUStats.SystemUsage + data.CPUPercent = calculateCPUPercentUnix(stats) + data.PercpuUsage = len(stats.CPUStats.CPUUsage.PercpuUsage) + + data.MemoryCache = stats.MemoryStats.Stats["cache"] + data.MemoryUsage = stats.MemoryStats.Usage + data.MemoryLimit = stats.MemoryStats.Limit + + data.MemoryPercent = calculateMemPercentUnix(stats.MemoryStats) + return data +} + +func checkPortStats(ports []dto.PortHelper) (nat.PortMap, error) { + portMap := make(nat.PortMap) + if len(ports) == 0 { + return portMap, nil + } + for _, port := range ports { + if strings.Contains(port.ContainerPort, "-") { + if !strings.Contains(port.HostPort, "-") { + return portMap, buserr.New(constant.ErrPortRules) + } + hostStart, _ := strconv.Atoi(strings.Split(port.HostPort, "-")[0]) + hostEnd, _ := strconv.Atoi(strings.Split(port.HostPort, "-")[1]) + containerStart, _ := strconv.Atoi(strings.Split(port.ContainerPort, "-")[0]) + containerEnd, _ := strconv.Atoi(strings.Split(port.ContainerPort, "-")[1]) + if (hostEnd-hostStart) <= 0 || (containerEnd-containerStart) <= 0 { + return portMap, buserr.New(constant.ErrPortRules) + } + if (containerEnd - containerStart) != (hostEnd - hostStart) { + return portMap, buserr.New(constant.ErrPortRules) + } + for i := 0; i <= hostEnd-hostStart; i++ { + bindItem := nat.PortBinding{HostPort: strconv.Itoa(hostStart + i), HostIP: port.HostIP} + portMap[nat.Port(fmt.Sprintf("%d/%s", containerStart+i, port.Protocol))] = []nat.PortBinding{bindItem} + } + for i := hostStart; i <= hostEnd; i++ { + if common.ScanPort(i) { + return portMap, buserr.WithDetail(constant.ErrPortInUsed, i, nil) + } + } + } else { + portItem := 0 + if strings.Contains(port.HostPort, "-") { + portItem, _ = strconv.Atoi(strings.Split(port.HostPort, "-")[0]) + } else { + portItem, _ = strconv.Atoi(port.HostPort) + } + if common.ScanPort(portItem) { + return portMap, buserr.WithDetail(constant.ErrPortInUsed, portItem, nil) + } + bindItem := nat.PortBinding{HostPort: strconv.Itoa(portItem), HostIP: port.HostIP} + portMap[nat.Port(fmt.Sprintf("%s/%s", port.ContainerPort, port.Protocol))] = []nat.PortBinding{bindItem} + } + } + return portMap, nil +} + +func loadConfigInfo(isCreate bool, req dto.ContainerOperate, oldContainer *types.ContainerJSON) (*container.Config, *container.HostConfig, *network.NetworkingConfig, error) { + var config container.Config + var hostConf container.HostConfig + if !isCreate { + config = *oldContainer.Config + hostConf = *oldContainer.HostConfig + } + var networkConf network.NetworkingConfig + + portMap, err := checkPortStats(req.ExposedPorts) + if err != nil { + return nil, nil, nil, err + } + exposed := make(nat.PortSet) + for port := range portMap { + exposed[port] = struct{}{} + } + config.Image = req.Image + config.Cmd = req.Cmd + config.Entrypoint = req.Entrypoint + config.Env = req.Env + config.Labels = stringsToMap(req.Labels) + config.ExposedPorts = exposed + config.OpenStdin = req.OpenStdin + config.Tty = req.Tty + + if len(req.Network) != 0 { + switch req.Network { + case "host", "none", "bridge": + hostConf.NetworkMode = container.NetworkMode(req.Network) + } + if req.Ipv4 != "" || req.Ipv6 != "" { + networkConf.EndpointsConfig = map[string]*network.EndpointSettings{ + req.Network: { + IPAMConfig: &network.EndpointIPAMConfig{ + IPv4Address: req.Ipv4, + IPv6Address: req.Ipv6, + }, + }} + } else { + networkConf.EndpointsConfig = map[string]*network.EndpointSettings{req.Network: {}} + } + } else { + if req.Ipv4 != "" || req.Ipv6 != "" { + return nil, nil, nil, fmt.Errorf("please set up the network") + } + networkConf = network.NetworkingConfig{} + } + + hostConf.Privileged = req.Privileged + hostConf.AutoRemove = req.AutoRemove + hostConf.CPUShares = req.CPUShares + hostConf.PublishAllPorts = req.PublishAllPorts + hostConf.RestartPolicy = container.RestartPolicy{Name: container.RestartPolicyMode(req.RestartPolicy)} + if req.RestartPolicy == "on-failure" { + hostConf.RestartPolicy.MaximumRetryCount = 5 + } + hostConf.NanoCPUs = int64(req.NanoCPUs * 1000000000) + hostConf.Memory = int64(req.Memory * 1024 * 1024) + hostConf.MemorySwap = 0 + hostConf.PortBindings = portMap + hostConf.Binds = []string{} + hostConf.Mounts = []mount.Mount{} + config.Volumes = make(map[string]struct{}) + for _, volume := range req.Volumes { + if volume.Type == "volume" { + hostConf.Mounts = append(hostConf.Mounts, mount.Mount{ + Type: mount.Type(volume.Type), + Source: volume.SourceDir, + Target: volume.ContainerDir, + }) + config.Volumes[volume.ContainerDir] = struct{}{} + } else { + hostConf.Binds = append(hostConf.Binds, fmt.Sprintf("%s:%s:%s", volume.SourceDir, volume.ContainerDir, volume.Mode)) + } + } + return &config, &hostConf, &networkConf, nil +} + +func reCreateAfterUpdate(name string, client *client.Client, config *container.Config, hostConf *container.HostConfig, networkConf *types.NetworkSettings) { + ctx := context.Background() + + var oldNetworkConf network.NetworkingConfig + if networkConf != nil { + for networkKey := range networkConf.Networks { + oldNetworkConf.EndpointsConfig = map[string]*network.EndpointSettings{networkKey: {}} + break + } + } + + oldContainer, err := client.ContainerCreate(ctx, config, hostConf, &oldNetworkConf, &v1.Platform{}, name) + if err != nil { + global.LOG.Errorf("recreate after container update failed, err: %v", err) + return + } + if err := client.ContainerStart(ctx, oldContainer.ID, container.StartOptions{}); err != nil { + global.LOG.Errorf("restart after container update failed, err: %v", err) + } + global.LOG.Errorf("recreate after container update successful") +} + +func loadVolumeBinds(binds []types.MountPoint) []dto.VolumeHelper { + var datas []dto.VolumeHelper + for _, bind := range binds { + var volumeItem dto.VolumeHelper + volumeItem.Type = string(bind.Type) + if bind.Type == "volume" { + volumeItem.SourceDir = bind.Name + } else { + volumeItem.SourceDir = bind.Source + } + volumeItem.ContainerDir = bind.Destination + volumeItem.Mode = "ro" + if bind.RW { + volumeItem.Mode = "rw" + } + datas = append(datas, volumeItem) + } + return datas +} + +func loadContainerPort(ports []types.Port) []string { + var ( + ipv4Ports []types.Port + ipv6Ports []types.Port + ) + for _, port := range ports { + if strings.Contains(port.IP, ":") { + ipv6Ports = append(ipv6Ports, port) + } else { + ipv4Ports = append(ipv4Ports, port) + } + } + list1 := simplifyPort(ipv4Ports) + list2 := simplifyPort(ipv6Ports) + return append(list1, list2...) +} +func simplifyPort(ports []types.Port) []string { + var datas []string + if len(ports) == 0 { + return datas + } + if len(ports) == 1 { + ip := "" + if len(ports[0].IP) != 0 { + ip = ports[0].IP + ":" + } + itemPortStr := fmt.Sprintf("%s%v/%s", ip, ports[0].PrivatePort, ports[0].Type) + if ports[0].PublicPort != 0 { + itemPortStr = fmt.Sprintf("%s%v->%v/%s", ip, ports[0].PublicPort, ports[0].PrivatePort, ports[0].Type) + } + datas = append(datas, itemPortStr) + return datas + } + + sort.Slice(ports, func(i, j int) bool { + return ports[i].PrivatePort < ports[j].PrivatePort + }) + start := ports[0] + + for i := 1; i < len(ports); i++ { + if ports[i].PrivatePort != ports[i-1].PrivatePort+1 || ports[i].IP != ports[i-1].IP || ports[i].PublicPort != ports[i-1].PublicPort+1 || ports[i].Type != ports[i-1].Type { + if ports[i-1].PrivatePort == start.PrivatePort { + itemPortStr := fmt.Sprintf("%s:%v/%s", start.IP, start.PrivatePort, start.Type) + if start.PublicPort != 0 { + itemPortStr = fmt.Sprintf("%s:%v->%v/%s", start.IP, start.PublicPort, start.PrivatePort, start.Type) + } + if len(start.IP) == 0 { + itemPortStr = strings.TrimPrefix(itemPortStr, ":") + } + datas = append(datas, itemPortStr) + } else { + itemPortStr := fmt.Sprintf("%s:%v-%v/%s", start.IP, start.PrivatePort, ports[i-1].PrivatePort, start.Type) + if start.PublicPort != 0 { + itemPortStr = fmt.Sprintf("%s:%v-%v->%v-%v/%s", start.IP, start.PublicPort, ports[i-1].PublicPort, start.PrivatePort, ports[i-1].PrivatePort, start.Type) + } + if len(start.IP) == 0 { + itemPortStr = strings.TrimPrefix(itemPortStr, ":") + } + datas = append(datas, itemPortStr) + } + start = ports[i] + } + if i == len(ports)-1 { + if ports[i].PrivatePort == start.PrivatePort { + itemPortStr := fmt.Sprintf("%s:%v/%s", start.IP, start.PrivatePort, start.Type) + if start.PublicPort != 0 { + itemPortStr = fmt.Sprintf("%s:%v->%v/%s", start.IP, start.PublicPort, start.PrivatePort, start.Type) + } + if len(start.IP) == 0 { + itemPortStr = strings.TrimPrefix(itemPortStr, ":") + } + datas = append(datas, itemPortStr) + } else { + itemPortStr := fmt.Sprintf("%s:%v-%v/%s", start.IP, start.PrivatePort, ports[i].PrivatePort, start.Type) + if start.PublicPort != 0 { + itemPortStr = fmt.Sprintf("%s:%v-%v->%v-%v/%s", start.IP, start.PublicPort, ports[i].PublicPort, start.PrivatePort, ports[i].PrivatePort, start.Type) + } + if len(start.IP) == 0 { + itemPortStr = strings.TrimPrefix(itemPortStr, ":") + } + datas = append(datas, itemPortStr) + } + } + } + return datas +} diff --git a/agent/app/service/container_compose.go b/agent/app/service/container_compose.go new file mode 100644 index 000000000..ca48ff6f3 --- /dev/null +++ b/agent/app/service/container_compose.go @@ -0,0 +1,282 @@ +package service + +import ( + "bufio" + "errors" + "fmt" + "io" + "os" + "os/exec" + "path" + "sort" + "strings" + "time" + + "github.com/docker/docker/api/types/container" + + "github.com/1Panel-dev/1Panel/agent/app/dto" + "github.com/1Panel-dev/1Panel/agent/app/model" + "github.com/1Panel-dev/1Panel/agent/buserr" + "github.com/1Panel-dev/1Panel/agent/constant" + "github.com/1Panel-dev/1Panel/agent/global" + "github.com/1Panel-dev/1Panel/agent/utils/cmd" + "github.com/1Panel-dev/1Panel/agent/utils/compose" + "github.com/1Panel-dev/1Panel/agent/utils/docker" + "github.com/docker/docker/api/types/filters" + "golang.org/x/net/context" +) + +const composeProjectLabel = "com.docker.compose.project" +const composeConfigLabel = "com.docker.compose.project.config_files" +const composeWorkdirLabel = "com.docker.compose.project.working_dir" +const composeCreatedBy = "createdBy" + +func (u *ContainerService) PageCompose(req dto.SearchWithPage) (int64, interface{}, error) { + var ( + records []dto.ComposeInfo + BackDatas []dto.ComposeInfo + ) + client, err := docker.NewDockerClient() + if err != nil { + return 0, nil, err + } + defer client.Close() + + options := container.ListOptions{All: true} + options.Filters = filters.NewArgs() + options.Filters.Add("label", composeProjectLabel) + + list, err := client.ContainerList(context.Background(), options) + if err != nil { + return 0, nil, err + } + + composeCreatedByLocal, _ := composeRepo.ListRecord() + composeMap := make(map[string]dto.ComposeInfo) + for _, container := range list { + if name, ok := container.Labels[composeProjectLabel]; ok { + containerItem := dto.ComposeContainer{ + ContainerID: container.ID, + Name: container.Names[0][1:], + State: container.State, + CreateTime: time.Unix(container.Created, 0).Format(constant.DateTimeLayout), + } + if compose, has := composeMap[name]; has { + compose.ContainerNumber++ + compose.Containers = append(compose.Containers, containerItem) + composeMap[name] = compose + } else { + config := container.Labels[composeConfigLabel] + workdir := container.Labels[composeWorkdirLabel] + composeItem := dto.ComposeInfo{ + ContainerNumber: 1, + CreatedAt: time.Unix(container.Created, 0).Format(constant.DateTimeLayout), + ConfigFile: config, + Workdir: workdir, + Containers: []dto.ComposeContainer{containerItem}, + } + createdBy, ok := container.Labels[composeCreatedBy] + if ok { + composeItem.CreatedBy = createdBy + } + if len(config) != 0 && len(workdir) != 0 && strings.Contains(config, workdir) { + composeItem.Path = config + } else { + composeItem.Path = workdir + } + for i := 0; i < len(composeCreatedByLocal); i++ { + if composeCreatedByLocal[i].Name == name { + composeItem.CreatedBy = "1Panel" + composeCreatedByLocal = append(composeCreatedByLocal[:i], composeCreatedByLocal[i+1:]...) + break + } + } + composeMap[name] = composeItem + } + } + } + for _, item := range composeCreatedByLocal { + if err := composeRepo.DeleteRecord(commonRepo.WithByID(item.ID)); err != nil { + global.LOG.Error(err) + } + } + for key, value := range composeMap { + value.Name = key + records = append(records, value) + } + if len(req.Info) != 0 { + length, count := len(records), 0 + for count < length { + if !strings.Contains(records[count].Name, req.Info) { + records = append(records[:count], records[(count+1):]...) + length-- + } else { + count++ + } + } + } + sort.Slice(records, func(i, j int) bool { + return records[i].CreatedAt > records[j].CreatedAt + }) + total, start, end := len(records), (req.Page-1)*req.PageSize, req.Page*req.PageSize + if start > total { + BackDatas = make([]dto.ComposeInfo, 0) + } else { + if end >= total { + end = total + } + BackDatas = records[start:end] + } + return int64(total), BackDatas, nil +} + +func (u *ContainerService) TestCompose(req dto.ComposeCreate) (bool, error) { + if cmd.CheckIllegal(req.Path) { + return false, buserr.New(constant.ErrCmdIllegal) + } + composeItem, _ := composeRepo.GetRecord(commonRepo.WithByName(req.Name)) + if composeItem.ID != 0 { + return false, constant.ErrRecordExist + } + if err := u.loadPath(&req); err != nil { + return false, err + } + cmd := exec.Command("docker-compose", "-f", req.Path, "config") + stdout, err := cmd.CombinedOutput() + if err != nil { + return false, errors.New(string(stdout)) + } + return true, nil +} + +func (u *ContainerService) CreateCompose(req dto.ComposeCreate) (string, error) { + if cmd.CheckIllegal(req.Name, req.Path) { + return "", buserr.New(constant.ErrCmdIllegal) + } + if err := u.loadPath(&req); err != nil { + return "", err + } + global.LOG.Infof("docker-compose.yml %s create successful, start to docker-compose up", req.Name) + + if req.From == "path" { + req.Name = path.Base(path.Dir(req.Path)) + } + + dockerLogDir := path.Join(global.CONF.System.TmpDir, "docker_logs") + if _, err := os.Stat(dockerLogDir); err != nil && os.IsNotExist(err) { + if err = os.MkdirAll(dockerLogDir, os.ModePerm); err != nil { + return "", err + } + } + logItem := fmt.Sprintf("%s/compose_create_%s_%s.log", dockerLogDir, req.Name, time.Now().Format(constant.DateTimeSlimLayout)) + file, err := os.OpenFile(logItem, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0666) + if err != nil { + return "", err + } + go func() { + defer file.Close() + cmd := exec.Command("docker-compose", "-f", req.Path, "up", "-d") + multiWriter := io.MultiWriter(os.Stdout, file) + cmd.Stdout = multiWriter + cmd.Stderr = multiWriter + if err := cmd.Run(); err != nil { + global.LOG.Errorf("docker-compose up %s failed, err: %v", req.Name, err) + _, _ = compose.Down(req.Path) + _, _ = file.WriteString("docker-compose up failed!") + return + } + global.LOG.Infof("docker-compose up %s successful!", req.Name) + _ = composeRepo.CreateRecord(&model.Compose{Name: req.Name}) + _, _ = file.WriteString("docker-compose up successful!") + }() + + return path.Base(logItem), nil +} + +func (u *ContainerService) ComposeOperation(req dto.ComposeOperation) error { + if cmd.CheckIllegal(req.Path, req.Operation) { + return buserr.New(constant.ErrCmdIllegal) + } + if _, err := os.Stat(req.Path); err != nil { + return fmt.Errorf("load file with path %s failed, %v", req.Path, err) + } + if stdout, err := compose.Operate(req.Path, req.Operation); err != nil { + return errors.New(string(stdout)) + } + global.LOG.Infof("docker-compose %s %s successful", req.Operation, req.Name) + if req.Operation == "down" { + _ = composeRepo.DeleteRecord(commonRepo.WithByName(req.Name)) + if req.WithFile { + _ = os.RemoveAll(path.Dir(req.Path)) + } + } + + return nil +} + +func (u *ContainerService) ComposeUpdate(req dto.ComposeUpdate) error { + if cmd.CheckIllegal(req.Name, req.Path) { + return buserr.New(constant.ErrCmdIllegal) + } + oldFile, err := os.ReadFile(req.Path) + if err != nil { + return fmt.Errorf("load file with path %s failed, %v", req.Path, err) + } + file, err := os.OpenFile(req.Path, os.O_WRONLY|os.O_TRUNC, 0640) + if err != nil { + return err + } + defer file.Close() + write := bufio.NewWriter(file) + _, _ = write.WriteString(req.Content) + write.Flush() + + global.LOG.Infof("docker-compose.yml %s has been replaced, now start to docker-compose restart", req.Path) + if stdout, err := compose.Up(req.Path); err != nil { + if err := recreateCompose(string(oldFile), req.Path); err != nil { + return fmt.Errorf("update failed when handle compose up, err: %s, recreate failed: %v", string(stdout), err) + } + return fmt.Errorf("update failed when handle compose up, err: %s", string(stdout)) + } + + return nil +} + +func (u *ContainerService) loadPath(req *dto.ComposeCreate) error { + if req.From == "template" || req.From == "edit" { + dir := fmt.Sprintf("%s/docker/compose/%s", constant.DataDir, req.Name) + if _, err := os.Stat(dir); err != nil && os.IsNotExist(err) { + if err = os.MkdirAll(dir, os.ModePerm); err != nil { + return err + } + } + + path := fmt.Sprintf("%s/docker-compose.yml", dir) + file, err := os.OpenFile(path, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0666) + if err != nil { + return err + } + defer file.Close() + write := bufio.NewWriter(file) + _, _ = write.WriteString(string(req.File)) + write.Flush() + req.Path = path + } + return nil +} + +func recreateCompose(content, path string) error { + file, err := os.OpenFile(path, os.O_WRONLY|os.O_TRUNC, 0640) + if err != nil { + return err + } + defer file.Close() + write := bufio.NewWriter(file) + _, _ = write.WriteString(content) + write.Flush() + + if stdout, err := compose.Up(path); err != nil { + return errors.New(string(stdout)) + } + return nil +} diff --git a/agent/app/service/container_network.go b/agent/app/service/container_network.go new file mode 100644 index 000000000..e894d4bd9 --- /dev/null +++ b/agent/app/service/container_network.go @@ -0,0 +1,179 @@ +package service + +import ( + "context" + "fmt" + "sort" + "strings" + + "github.com/1Panel-dev/1Panel/agent/app/dto" + "github.com/1Panel-dev/1Panel/agent/buserr" + "github.com/1Panel-dev/1Panel/agent/constant" + "github.com/1Panel-dev/1Panel/agent/utils/docker" + "github.com/docker/docker/api/types" + "github.com/docker/docker/api/types/network" +) + +func (u *ContainerService) PageNetwork(req dto.SearchWithPage) (int64, interface{}, error) { + client, err := docker.NewDockerClient() + if err != nil { + return 0, nil, err + } + defer client.Close() + list, err := client.NetworkList(context.TODO(), types.NetworkListOptions{}) + if err != nil { + return 0, nil, err + } + if len(req.Info) != 0 { + length, count := len(list), 0 + for count < length { + if !strings.Contains(list[count].Name, req.Info) { + list = append(list[:count], list[(count+1):]...) + length-- + } else { + count++ + } + } + } + var ( + data []dto.Network + records []network.Inspect + ) + sort.Slice(list, func(i, j int) bool { + return list[i].Created.Before(list[j].Created) + }) + total, start, end := len(list), (req.Page-1)*req.PageSize, req.Page*req.PageSize + if start > total { + records = make([]network.Inspect, 0) + } else { + if end >= total { + end = total + } + records = list[start:end] + } + + for _, item := range records { + tag := make([]string, 0) + for key, val := range item.Labels { + tag = append(tag, fmt.Sprintf("%s=%s", key, val)) + } + var ipam network.IPAMConfig + if len(item.IPAM.Config) > 0 { + ipam = item.IPAM.Config[0] + } + data = append(data, dto.Network{ + ID: item.ID, + CreatedAt: item.Created, + Name: item.Name, + Driver: item.Driver, + IPAMDriver: item.IPAM.Driver, + Subnet: ipam.Subnet, + Gateway: ipam.Gateway, + Attachable: item.Attachable, + Labels: tag, + }) + } + + return int64(total), data, nil +} + +func (u *ContainerService) ListNetwork() ([]dto.Options, error) { + client, err := docker.NewDockerClient() + if err != nil { + return nil, err + } + defer client.Close() + list, err := client.NetworkList(context.TODO(), types.NetworkListOptions{}) + if err != nil { + return nil, err + } + var datas []dto.Options + for _, item := range list { + datas = append(datas, dto.Options{Option: item.Name}) + } + sort.Slice(datas, func(i, j int) bool { + return datas[i].Option < datas[j].Option + }) + return datas, nil +} + +func (u *ContainerService) DeleteNetwork(req dto.BatchDelete) error { + client, err := docker.NewDockerClient() + if err != nil { + return err + } + defer client.Close() + for _, id := range req.Names { + if err := client.NetworkRemove(context.TODO(), id); err != nil { + if strings.Contains(err.Error(), "has active endpoints") { + return buserr.WithDetail(constant.ErrInUsed, id, nil) + } + return err + } + } + return nil +} +func (u *ContainerService) CreateNetwork(req dto.NetworkCreate) error { + client, err := docker.NewDockerClient() + if err != nil { + return err + } + defer client.Close() + var ( + ipams []network.IPAMConfig + enableV6 bool + ) + if req.Ipv4 { + var itemIpam network.IPAMConfig + if len(req.AuxAddress) != 0 { + itemIpam.AuxAddress = make(map[string]string) + } + if len(req.Subnet) != 0 { + itemIpam.Subnet = req.Subnet + } + if len(req.Gateway) != 0 { + itemIpam.Gateway = req.Gateway + } + if len(req.IPRange) != 0 { + itemIpam.IPRange = req.IPRange + } + for _, addr := range req.AuxAddress { + itemIpam.AuxAddress[addr.Key] = addr.Value + } + ipams = append(ipams, itemIpam) + } + if req.Ipv6 { + enableV6 = true + var itemIpam network.IPAMConfig + if len(req.AuxAddress) != 0 { + itemIpam.AuxAddress = make(map[string]string) + } + if len(req.SubnetV6) != 0 { + itemIpam.Subnet = req.SubnetV6 + } + if len(req.GatewayV6) != 0 { + itemIpam.Gateway = req.GatewayV6 + } + if len(req.IPRangeV6) != 0 { + itemIpam.IPRange = req.IPRangeV6 + } + for _, addr := range req.AuxAddressV6 { + itemIpam.AuxAddress[addr.Key] = addr.Value + } + ipams = append(ipams, itemIpam) + } + + options := network.CreateOptions{ + EnableIPv6: &enableV6, + Driver: req.Driver, + Options: stringsToMap(req.Options), + Labels: stringsToMap(req.Labels), + } + if len(ipams) != 0 { + options.IPAM = &network.IPAM{Config: ipams} + } + if _, err := client.NetworkCreate(context.TODO(), req.Name, options); err != nil { + return err + } + return nil +} diff --git a/agent/app/service/container_volume.go b/agent/app/service/container_volume.go new file mode 100644 index 000000000..8e822d548 --- /dev/null +++ b/agent/app/service/container_volume.go @@ -0,0 +1,143 @@ +package service + +import ( + "context" + "sort" + "strings" + "time" + + "github.com/1Panel-dev/1Panel/agent/app/dto" + "github.com/1Panel-dev/1Panel/agent/buserr" + "github.com/1Panel-dev/1Panel/agent/constant" + "github.com/1Panel-dev/1Panel/agent/utils/common" + "github.com/1Panel-dev/1Panel/agent/utils/docker" + "github.com/docker/docker/api/types/filters" + "github.com/docker/docker/api/types/volume" +) + +func (u *ContainerService) PageVolume(req dto.SearchWithPage) (int64, interface{}, error) { + client, err := docker.NewDockerClient() + if err != nil { + return 0, nil, err + } + list, err := client.VolumeList(context.TODO(), volume.ListOptions{}) + if err != nil { + return 0, nil, err + } + if len(req.Info) != 0 { + length, count := len(list.Volumes), 0 + for count < length { + if !strings.Contains(list.Volumes[count].Name, req.Info) { + list.Volumes = append(list.Volumes[:count], list.Volumes[(count+1):]...) + length-- + } else { + count++ + } + } + } + var ( + data []dto.Volume + records []*volume.Volume + ) + sort.Slice(list.Volumes, func(i, j int) bool { + return list.Volumes[i].CreatedAt > list.Volumes[j].CreatedAt + }) + total, start, end := len(list.Volumes), (req.Page-1)*req.PageSize, req.Page*req.PageSize + if start > total { + records = make([]*volume.Volume, 0) + } else { + if end >= total { + end = total + } + records = list.Volumes[start:end] + } + + nyc, _ := time.LoadLocation(common.LoadTimeZone()) + for _, item := range records { + tag := make([]string, 0) + for _, val := range item.Labels { + tag = append(tag, val) + } + var createTime time.Time + if strings.Contains(item.CreatedAt, "Z") { + createTime, _ = time.ParseInLocation("2006-01-02T15:04:05Z", item.CreatedAt, nyc) + } else if strings.Contains(item.CreatedAt, "+") { + createTime, _ = time.ParseInLocation("2006-01-02T15:04:05+08:00", item.CreatedAt, nyc) + } else { + createTime, _ = time.ParseInLocation("2006-01-02T15:04:05", item.CreatedAt, nyc) + } + data = append(data, dto.Volume{ + CreatedAt: createTime, + Name: item.Name, + Driver: item.Driver, + Mountpoint: item.Mountpoint, + Labels: tag, + }) + } + + return int64(total), data, nil +} +func (u *ContainerService) ListVolume() ([]dto.Options, error) { + client, err := docker.NewDockerClient() + if err != nil { + return nil, err + } + defer client.Close() + list, err := client.VolumeList(context.TODO(), volume.ListOptions{}) + if err != nil { + return nil, err + } + var datas []dto.Options + for _, item := range list.Volumes { + datas = append(datas, dto.Options{ + Option: item.Name, + }) + } + sort.Slice(datas, func(i, j int) bool { + return datas[i].Option < datas[j].Option + }) + return datas, nil +} +func (u *ContainerService) DeleteVolume(req dto.BatchDelete) error { + client, err := docker.NewDockerClient() + if err != nil { + return err + } + defer client.Close() + for _, id := range req.Names { + if err := client.VolumeRemove(context.TODO(), id, true); err != nil { + if strings.Contains(err.Error(), "volume is in use") { + return buserr.WithDetail(constant.ErrInUsed, id, nil) + } + return err + } + } + return nil +} +func (u *ContainerService) CreateVolume(req dto.VolumeCreate) error { + client, err := docker.NewDockerClient() + if err != nil { + return err + } + defer client.Close() + arg := filters.NewArgs() + arg.Add("name", req.Name) + vos, _ := client.VolumeList(context.TODO(), volume.ListOptions{Filters: arg}) + if len(vos.Volumes) != 0 { + for _, v := range vos.Volumes { + if v.Name == req.Name { + return constant.ErrRecordExist + } + } + } + options := volume.CreateOptions{ + Name: req.Name, + Driver: req.Driver, + DriverOpts: stringsToMap(req.Options), + Labels: stringsToMap(req.Labels), + } + if _, err := client.VolumeCreate(context.TODO(), options); err != nil { + return err + } + return nil +} diff --git a/agent/app/service/cronjob.go b/agent/app/service/cronjob.go new file mode 100644 index 000000000..1725fc3d8 --- /dev/null +++ b/agent/app/service/cronjob.go @@ -0,0 +1,344 @@ +package service + +import ( + "bufio" + "fmt" + "os" + "path" + "strconv" + "strings" + "time" + + "github.com/1Panel-dev/1Panel/agent/app/dto" + "github.com/1Panel-dev/1Panel/agent/app/model" + "github.com/1Panel-dev/1Panel/agent/constant" + "github.com/1Panel-dev/1Panel/agent/global" + "github.com/jinzhu/copier" + "github.com/pkg/errors" + "github.com/robfig/cron/v3" +) + +type CronjobService struct{} + +type ICronjobService interface { + SearchWithPage(search dto.PageCronjob) (int64, interface{}, error) + SearchRecords(search dto.SearchRecord) (int64, interface{}, error) + Create(cronjobDto dto.CronjobCreate) error + HandleOnce(id uint) error + Update(id uint, req dto.CronjobUpdate) error + UpdateStatus(id uint, status string) error + Delete(req dto.CronjobBatchDelete) error + Download(down dto.CronjobDownload) (string, error) + StartJob(cronjob *model.Cronjob, isUpdate bool) (string, error) + CleanRecord(req dto.CronjobClean) error + + LoadRecordLog(req dto.OperateByID) string +} + +func NewICronjobService() ICronjobService { + return &CronjobService{} +} + +func (u *CronjobService) SearchWithPage(search dto.PageCronjob) (int64, interface{}, error) { + total, cronjobs, err := cronjobRepo.Page(search.Page, search.PageSize, commonRepo.WithLikeName(search.Info), commonRepo.WithOrderRuleBy(search.OrderBy, search.Order)) + var dtoCronjobs []dto.CronjobInfo + for _, cronjob := range cronjobs { + var item dto.CronjobInfo + if err := copier.Copy(&item, &cronjob); err != nil { + return 0, nil, errors.WithMessage(constant.ErrStructTransform, err.Error()) + } + record, _ := cronjobRepo.RecordFirst(cronjob.ID) + if record.ID != 0 { + item.LastRecordTime = record.StartTime.Format(constant.DateTimeLayout) + } else { + item.LastRecordTime = "-" + } + dtoCronjobs = append(dtoCronjobs, item) + } + return total, dtoCronjobs, err +} + +func (u *CronjobService) SearchRecords(search dto.SearchRecord) (int64, interface{}, error) { + total, records, err := cronjobRepo.PageRecords( + search.Page, + search.PageSize, + commonRepo.WithByStatus(search.Status), + cronjobRepo.WithByJobID(search.CronjobID), + commonRepo.WithByDate(search.StartTime, search.EndTime)) + var dtoCronjobs []dto.Record + for _, record := range records { + var item dto.Record + if err := copier.Copy(&item, &record); err != nil { + return 0, nil, errors.WithMessage(constant.ErrStructTransform, err.Error()) + } + item.StartTime = record.StartTime.Format(constant.DateTimeLayout) + dtoCronjobs = append(dtoCronjobs, item) + } + return total, dtoCronjobs, err +} + +func (u *CronjobService) LoadRecordLog(req dto.OperateByID) string { + record, err := cronjobRepo.GetRecord(commonRepo.WithByID(req.ID)) + if err != nil { + return "" + } + if _, err := os.Stat(record.Records); err != nil { + return "" + } + content, err := os.ReadFile(record.Records) + if err != nil { + return "" + } + return string(content) +} + +func (u *CronjobService) CleanRecord(req dto.CronjobClean) error { + cronjob, err := cronjobRepo.Get(commonRepo.WithByID(req.CronjobID)) + if err != nil { + return err + } + if req.CleanData { + if hasBackup(cronjob.Type) { + accountMap, err := loadClientMap(cronjob.BackupAccounts) + if err != nil { + return err + } + cronjob.RetainCopies = 0 + u.removeExpiredBackup(cronjob, accountMap, model.BackupRecord{}) + } else { + u.removeExpiredLog(cronjob) + } + } + if req.IsDelete { + records, _ := backupRepo.ListRecord(backupRepo.WithByCronID(cronjob.ID)) + for _, records := range records { + records.CronjobID = 0 + _ = backupRepo.UpdateRecord(&records) + } + } + delRecords, err := cronjobRepo.ListRecord(cronjobRepo.WithByJobID(int(req.CronjobID))) + if err != nil { + return err + } + for _, del := range delRecords { + _ = os.RemoveAll(del.Records) + } + if err := cronjobRepo.DeleteRecord(cronjobRepo.WithByJobID(int(req.CronjobID))); err != nil { + return err + } + return nil +} + +func (u *CronjobService) Download(down dto.CronjobDownload) (string, error) { + record, _ := cronjobRepo.GetRecord(commonRepo.WithByID(down.RecordID)) + if record.ID == 0 { + return "", constant.ErrRecordNotFound + } + backup, _ := backupRepo.Get(commonRepo.WithByID(down.BackupAccountID)) + if backup.ID == 0 { + return "", constant.ErrRecordNotFound + } + if backup.Type == "LOCAL" || record.FromLocal { + if _, err := os.Stat(record.File); err != nil && os.IsNotExist(err) { + return "", err + } + return record.File, nil + } + tempPath := fmt.Sprintf("%s/download/%s", constant.DataDir, record.File) + if _, err := os.Stat(tempPath); err != nil && os.IsNotExist(err) { + client, err := NewIBackupService().NewClient(&backup) + if err != nil { + return "", err + } + _ = os.MkdirAll(path.Dir(tempPath), os.ModePerm) + isOK, err := client.Download(record.File, tempPath) + if !isOK || err != nil { + return "", err + } + } + return tempPath, nil +} + +func (u *CronjobService) HandleOnce(id uint) error { + cronjob, _ := cronjobRepo.Get(commonRepo.WithByID(id)) + if cronjob.ID == 0 { + return constant.ErrRecordNotFound + } + u.HandleJob(&cronjob) + return nil +} + +func (u *CronjobService) Create(cronjobDto dto.CronjobCreate) error { + cronjob, _ := cronjobRepo.Get(commonRepo.WithByName(cronjobDto.Name)) + if cronjob.ID != 0 { + return constant.ErrRecordExist + } + cronjob.Secret = cronjobDto.Secret + if err := copier.Copy(&cronjob, &cronjobDto); err != nil { + return errors.WithMessage(constant.ErrStructTransform, err.Error()) + } + cronjob.Status = constant.StatusEnable + + global.LOG.Infof("create cronjob %s successful, spec: %s", cronjob.Name, cronjob.Spec) + spec := cronjob.Spec + entryIDs, err := u.StartJob(&cronjob, false) + if err != nil { + return err + } + cronjob.Spec = spec + cronjob.EntryIDs = entryIDs + if err := cronjobRepo.Create(&cronjob); err != nil { + return err + } + return nil +} + +func (u *CronjobService) StartJob(cronjob *model.Cronjob, isUpdate bool) (string, error) { + if len(cronjob.EntryIDs) != 0 && isUpdate { + ids := strings.Split(cronjob.EntryIDs, ",") + for _, id := range ids { + idItem, _ := strconv.Atoi(id) + global.Cron.Remove(cron.EntryID(idItem)) + } + } + specs := strings.Split(cronjob.Spec, ",") + var ids []string + for _, spec := range specs { + cronjob.Spec = spec + entryID, err := u.AddCronJob(cronjob) + if err != nil { + return "", err + } + ids = append(ids, fmt.Sprintf("%v", entryID)) + } + return strings.Join(ids, ","), nil +} + +func (u *CronjobService) Delete(req dto.CronjobBatchDelete) error { + for _, id := range req.IDs { + cronjob, _ := cronjobRepo.Get(commonRepo.WithByID(id)) + if cronjob.ID == 0 { + return errors.New("find cronjob in db failed") + } + ids := strings.Split(cronjob.EntryIDs, ",") + for _, id := range ids { + idItem, _ := strconv.Atoi(id) + global.Cron.Remove(cron.EntryID(idItem)) + } + global.LOG.Infof("stop cronjob entryID: %s", cronjob.EntryIDs) + if err := u.CleanRecord(dto.CronjobClean{CronjobID: id, CleanData: req.CleanData, IsDelete: true}); err != nil { + return err + } + if err := cronjobRepo.Delete(commonRepo.WithByID(id)); err != nil { + return err + } + } + + return nil +} + +func (u *CronjobService) Update(id uint, req dto.CronjobUpdate) error { + var cronjob model.Cronjob + if err := copier.Copy(&cronjob, &req); err != nil { + return errors.WithMessage(constant.ErrStructTransform, err.Error()) + } + cronModel, err := cronjobRepo.Get(commonRepo.WithByID(id)) + if err != nil { + return constant.ErrRecordNotFound + } + upMap := make(map[string]interface{}) + cronjob.EntryIDs = cronModel.EntryIDs + cronjob.Type = cronModel.Type + spec := cronjob.Spec + if cronModel.Status == constant.StatusEnable { + newEntryIDs, err := u.StartJob(&cronjob, true) + if err != nil { + return err + } + upMap["entry_ids"] = newEntryIDs + } else { + ids := strings.Split(cronjob.EntryIDs, ",") + for _, id := range ids { + idItem, _ := strconv.Atoi(id) + global.Cron.Remove(cron.EntryID(idItem)) + } + } + + upMap["name"] = req.Name + upMap["spec"] = spec + upMap["script"] = req.Script + upMap["command"] = req.Command + upMap["container_name"] = req.ContainerName + upMap["app_id"] = req.AppID + upMap["website"] = req.Website + upMap["exclusion_rules"] = req.ExclusionRules + upMap["db_type"] = req.DBType + upMap["db_name"] = req.DBName + upMap["url"] = req.URL + upMap["source_dir"] = req.SourceDir + + upMap["backup_accounts"] = req.BackupAccounts + upMap["default_download"] = req.DefaultDownload + upMap["retain_copies"] = req.RetainCopies + upMap["secret"] = req.Secret + return cronjobRepo.Update(id, upMap) +} + +func (u *CronjobService) UpdateStatus(id uint, status string) error { + cronjob, _ := cronjobRepo.Get(commonRepo.WithByID(id)) + if cronjob.ID == 0 { + return errors.WithMessage(constant.ErrRecordNotFound, "record not found") + } + var ( + entryIDs string + err error + ) + if status == constant.StatusEnable { + entryIDs, err = u.StartJob(&cronjob, false) + if err != nil { + return err + } + } else { + ids := strings.Split(cronjob.EntryIDs, ",") + for _, id := range ids { + idItem, _ := strconv.Atoi(id) + global.Cron.Remove(cron.EntryID(idItem)) + } + global.LOG.Infof("stop cronjob entryID: %s", cronjob.EntryIDs) + } + return cronjobRepo.Update(cronjob.ID, map[string]interface{}{"status": status, "entry_ids": entryIDs}) +} + +func (u *CronjobService) AddCronJob(cronjob *model.Cronjob) (int, error) { + addFunc := func() { + u.HandleJob(cronjob) + } + global.LOG.Infof("add %s job %s successful", cronjob.Type, cronjob.Name) + entryID, err := global.Cron.AddFunc(cronjob.Spec, addFunc) + if err != nil { + return 0, err + } + global.LOG.Infof("start cronjob entryID: %d", entryID) + return int(entryID), nil +} + +func mkdirAndWriteFile(cronjob *model.Cronjob, startTime time.Time, msg []byte) (string, error) { + dir := fmt.Sprintf("%s/task/%s/%s", constant.DataDir, cronjob.Type, cronjob.Name) + if _, err := os.Stat(dir); err != nil && os.IsNotExist(err) { + if err = os.MkdirAll(dir, os.ModePerm); err != nil { + return "", err + } + } + + path := fmt.Sprintf("%s/%s.log", dir, startTime.Format(constant.DateTimeSlimLayout)) + global.LOG.Infof("cronjob %s has generated some logs %s", cronjob.Name, path) + file, err := os.OpenFile(path, os.O_WRONLY|os.O_CREATE, 0666) + if err != nil { + return "", err + } + defer file.Close() + write := bufio.NewWriter(file) + _, _ = write.WriteString(string(msg)) + write.Flush() + return path, nil +} diff --git a/agent/app/service/cronjob_backup.go b/agent/app/service/cronjob_backup.go new file mode 100644 index 000000000..2391c7445 --- /dev/null +++ b/agent/app/service/cronjob_backup.go @@ -0,0 +1,386 @@ +package service + +import ( + "fmt" + "os" + "path" + "strconv" + "strings" + "time" + + "github.com/1Panel-dev/1Panel/agent/app/dto" + "github.com/1Panel-dev/1Panel/agent/app/model" + "github.com/1Panel-dev/1Panel/agent/constant" + "github.com/1Panel-dev/1Panel/agent/global" + "github.com/1Panel-dev/1Panel/agent/utils/common" +) + +func (u *CronjobService) handleApp(cronjob model.Cronjob, startTime time.Time) error { + var apps []model.AppInstall + if cronjob.AppID == "all" { + apps, _ = appInstallRepo.ListBy() + } else { + itemID, _ := strconv.Atoi(cronjob.AppID) + app, err := appInstallRepo.GetFirst(commonRepo.WithByID(uint(itemID))) + if err != nil { + return err + } + apps = append(apps, app) + } + accountMap, err := loadClientMap(cronjob.BackupAccounts) + if err != nil { + return err + } + for _, app := range apps { + var record model.BackupRecord + record.From = "cronjob" + record.Type = "app" + record.CronjobID = cronjob.ID + record.Name = app.App.Key + record.DetailName = app.Name + record.Source, record.BackupType = loadRecordPath(cronjob, accountMap) + backupDir := path.Join(global.CONF.System.TmpDir, fmt.Sprintf("app/%s/%s", app.App.Key, app.Name)) + record.FileName = fmt.Sprintf("app_%s_%s.tar.gz", app.Name, startTime.Format(constant.DateTimeSlimLayout)+common.RandStrAndNum(5)) + if err := handleAppBackup(&app, backupDir, record.FileName, cronjob.ExclusionRules, cronjob.Secret); err != nil { + return err + } + downloadPath, err := u.uploadCronjobBackFile(cronjob, accountMap, path.Join(backupDir, record.FileName)) + if err != nil { + return err + } + record.FileDir = path.Dir(downloadPath) + if err := backupRepo.CreateRecord(&record); err != nil { + global.LOG.Errorf("save backup record failed, err: %v", err) + return err + } + u.removeExpiredBackup(cronjob, accountMap, record) + } + return nil +} + +func (u *CronjobService) handleWebsite(cronjob model.Cronjob, startTime time.Time) error { + webs := loadWebsForJob(cronjob) + accountMap, err := loadClientMap(cronjob.BackupAccounts) + if err != nil { + return err + } + for _, web := range webs { + var record model.BackupRecord + record.From = "cronjob" + record.Type = "website" + record.CronjobID = cronjob.ID + record.Name = web.PrimaryDomain + record.DetailName = web.Alias + record.Source, record.BackupType = loadRecordPath(cronjob, accountMap) + backupDir := path.Join(global.CONF.System.TmpDir, fmt.Sprintf("website/%s", web.PrimaryDomain)) + record.FileName = fmt.Sprintf("website_%s_%s.tar.gz", web.PrimaryDomain, startTime.Format(constant.DateTimeSlimLayout)+common.RandStrAndNum(5)) + if err := handleWebsiteBackup(&web, backupDir, record.FileName, cronjob.ExclusionRules, cronjob.Secret); err != nil { + return err + } + downloadPath, err := u.uploadCronjobBackFile(cronjob, accountMap, path.Join(backupDir, record.FileName)) + if err != nil { + return err + } + record.FileDir = path.Dir(downloadPath) + if err := backupRepo.CreateRecord(&record); err != nil { + global.LOG.Errorf("save backup record failed, err: %v", err) + return err + } + u.removeExpiredBackup(cronjob, accountMap, record) + } + return nil +} + +func (u *CronjobService) handleDatabase(cronjob model.Cronjob, startTime time.Time) error { + dbs := loadDbsForJob(cronjob) + accountMap, err := loadClientMap(cronjob.BackupAccounts) + if err != nil { + return err + } + for _, dbInfo := range dbs { + var record model.BackupRecord + record.From = "cronjob" + record.Type = dbInfo.DBType + record.CronjobID = cronjob.ID + record.Name = dbInfo.Database + record.DetailName = dbInfo.Name + record.Source, record.BackupType = loadRecordPath(cronjob, accountMap) + + 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(constant.DateTimeSlimLayout)+common.RandStrAndNum(5)) + if cronjob.DBType == "mysql" || cronjob.DBType == "mariadb" { + if err := handleMysqlBackup(dbInfo.Database, dbInfo.DBType, dbInfo.Name, backupDir, record.FileName); err != nil { + return err + } + } else { + if err := handlePostgresqlBackup(dbInfo.Database, dbInfo.Name, backupDir, record.FileName); err != nil { + return err + } + } + downloadPath, err := u.uploadCronjobBackFile(cronjob, accountMap, path.Join(backupDir, record.FileName)) + if err != nil { + return err + } + record.FileDir = path.Dir(downloadPath) + if err := backupRepo.CreateRecord(&record); err != nil { + global.LOG.Errorf("save backup record failed, err: %v", err) + return err + } + u.removeExpiredBackup(cronjob, accountMap, record) + } + return nil +} + +func (u *CronjobService) handleDirectory(cronjob model.Cronjob, startTime time.Time) error { + accountMap, err := loadClientMap(cronjob.BackupAccounts) + if err != nil { + return err + } + fileName := fmt.Sprintf("directory%s_%s.tar.gz", strings.ReplaceAll(cronjob.SourceDir, "/", "_"), startTime.Format(constant.DateTimeSlimLayout)+common.RandStrAndNum(5)) + backupDir := path.Join(global.CONF.System.TmpDir, fmt.Sprintf("%s/%s", cronjob.Type, cronjob.Name)) + if err := handleTar(cronjob.SourceDir, backupDir, fileName, cronjob.ExclusionRules, cronjob.Secret); err != nil { + return err + } + var record model.BackupRecord + record.From = "cronjob" + record.Type = "directory" + record.CronjobID = cronjob.ID + record.Name = cronjob.Name + record.Source, record.BackupType = loadRecordPath(cronjob, accountMap) + downloadPath, err := u.uploadCronjobBackFile(cronjob, accountMap, path.Join(backupDir, fileName)) + if err != nil { + return err + } + record.FileDir = path.Dir(downloadPath) + record.FileName = fileName + if err := backupRepo.CreateRecord(&record); err != nil { + global.LOG.Errorf("save backup record failed, err: %v", err) + return err + } + u.removeExpiredBackup(cronjob, accountMap, record) + return nil +} + +func (u *CronjobService) handleSystemLog(cronjob model.Cronjob, startTime time.Time) error { + accountMap, err := loadClientMap(cronjob.BackupAccounts) + if err != nil { + return err + } + nameItem := startTime.Format(constant.DateTimeSlimLayout) + common.RandStrAndNum(5) + fileName := fmt.Sprintf("system_log_%s.tar.gz", nameItem) + backupDir := path.Join(global.CONF.System.TmpDir, "log", nameItem) + if err := handleBackupLogs(backupDir, fileName, cronjob.Secret); err != nil { + return err + } + var record model.BackupRecord + record.From = "cronjob" + record.Type = "log" + record.CronjobID = cronjob.ID + record.Name = cronjob.Name + record.Source, record.BackupType = loadRecordPath(cronjob, accountMap) + downloadPath, err := u.uploadCronjobBackFile(cronjob, accountMap, path.Join(path.Dir(backupDir), fileName)) + if err != nil { + return err + } + record.FileDir = path.Dir(downloadPath) + record.FileName = fileName + if err := backupRepo.CreateRecord(&record); err != nil { + global.LOG.Errorf("save backup record failed, err: %v", err) + return err + } + u.removeExpiredBackup(cronjob, accountMap, record) + return nil +} + +func (u *CronjobService) handleSnapshot(cronjob model.Cronjob, startTime time.Time, logPath string) error { + accountMap, err := loadClientMap(cronjob.BackupAccounts) + if err != nil { + return err + } + + var record model.BackupRecord + record.From = "cronjob" + record.Type = "directory" + record.CronjobID = cronjob.ID + record.Name = cronjob.Name + record.Source, record.BackupType = loadRecordPath(cronjob, accountMap) + record.FileDir = "system_snapshot" + + req := dto.SnapshotCreate{ + From: record.BackupType, + DefaultDownload: cronjob.DefaultDownload, + } + name, err := NewISnapshotService().HandleSnapshot(true, logPath, req, startTime.Format(constant.DateTimeSlimLayout)+common.RandStrAndNum(5), cronjob.Secret) + if err != nil { + return err + } + record.FileName = name + ".tar.gz" + + if err := backupRepo.CreateRecord(&record); err != nil { + global.LOG.Errorf("save backup record failed, err: %v", err) + return err + } + u.removeExpiredBackup(cronjob, accountMap, record) + return nil +} + +type databaseHelper struct { + DBType string + Database string + Name string +} + +func loadDbsForJob(cronjob model.Cronjob) []databaseHelper { + var dbs []databaseHelper + if cronjob.DBName == "all" { + if cronjob.DBType == "mysql" || cronjob.DBType == "mariadb" { + mysqlItems, _ := mysqlRepo.List() + for _, mysql := range mysqlItems { + dbs = append(dbs, databaseHelper{ + DBType: cronjob.DBType, + Database: mysql.MysqlName, + Name: mysql.Name, + }) + } + } else { + pgItems, _ := postgresqlRepo.List() + for _, pg := range pgItems { + dbs = append(dbs, databaseHelper{ + DBType: cronjob.DBType, + Database: pg.PostgresqlName, + Name: pg.Name, + }) + } + } + return dbs + } + itemID, _ := strconv.Atoi(cronjob.DBName) + if cronjob.DBType == "mysql" || cronjob.DBType == "mariadb" { + mysqlItem, _ := mysqlRepo.Get(commonRepo.WithByID(uint(itemID))) + dbs = append(dbs, databaseHelper{ + DBType: cronjob.DBType, + Database: mysqlItem.MysqlName, + Name: mysqlItem.Name, + }) + } else { + pgItem, _ := postgresqlRepo.Get(commonRepo.WithByID(uint(itemID))) + dbs = append(dbs, databaseHelper{ + DBType: cronjob.DBType, + Database: pgItem.PostgresqlName, + Name: pgItem.Name, + }) + } + return dbs +} + +func loadWebsForJob(cronjob model.Cronjob) []model.Website { + var weblist []model.Website + if cronjob.Website == "all" { + weblist, _ = websiteRepo.List() + return weblist + } + itemID, _ := strconv.Atoi(cronjob.Website) + webItem, _ := websiteRepo.GetFirst(commonRepo.WithByID(uint(itemID))) + if webItem.ID != 0 { + weblist = append(weblist, webItem) + } + return weblist +} + +func loadRecordPath(cronjob model.Cronjob, accountMap map[string]cronjobUploadHelper) (string, string) { + source := accountMap[fmt.Sprintf("%v", cronjob.DefaultDownload)].backType + targets := strings.Split(cronjob.BackupAccounts, ",") + var itemAccounts []string + for _, target := range targets { + if len(target) == 0 { + continue + } + if len(accountMap[target].backType) != 0 { + itemAccounts = append(itemAccounts, accountMap[target].backType) + } + } + backupType := strings.Join(itemAccounts, ",") + return source, backupType +} + +func handleBackupLogs(targetDir, fileName string, secret string) error { + websites, err := websiteRepo.List() + if err != nil { + return err + } + if len(websites) != 0 { + nginxInstall, err := getAppInstallByKey(constant.AppOpenresty) + if err != nil { + return err + } + webItem := path.Join(nginxInstall.GetPath(), "www/sites") + for _, website := range websites { + dirItem := path.Join(targetDir, "website", website.Alias) + if _, err := os.Stat(dirItem); err != nil && os.IsNotExist(err) { + if err = os.MkdirAll(dirItem, os.ModePerm); err != nil { + return err + } + } + itemDir := path.Join(webItem, website.Alias, "log") + logFiles, _ := os.ReadDir(itemDir) + if len(logFiles) != 0 { + for i := 0; i < len(logFiles); i++ { + if !logFiles[i].IsDir() { + _ = common.CopyFile(path.Join(itemDir, logFiles[i].Name()), dirItem) + } + } + } + itemDir2 := path.Join(global.CONF.System.Backup, "log/website", website.Alias) + logFiles2, _ := os.ReadDir(itemDir2) + if len(logFiles2) != 0 { + for i := 0; i < len(logFiles2); i++ { + if !logFiles2[i].IsDir() { + _ = common.CopyFile(path.Join(itemDir2, logFiles2[i].Name()), dirItem) + } + } + } + } + global.LOG.Debug("backup website log successful!") + } + + systemLogDir := path.Join(global.CONF.System.BaseDir, "1panel/log") + systemDir := path.Join(targetDir, "system") + if _, err := os.Stat(systemDir); err != nil && os.IsNotExist(err) { + if err = os.MkdirAll(systemDir, os.ModePerm); err != nil { + return err + } + } + systemLogFiles, _ := os.ReadDir(systemLogDir) + if len(systemLogFiles) != 0 { + for i := 0; i < len(systemLogFiles); i++ { + if !systemLogFiles[i].IsDir() { + _ = common.CopyFile(path.Join(systemLogDir, systemLogFiles[i].Name()), systemDir) + } + } + } + global.LOG.Debug("backup system log successful!") + + loginLogFiles, _ := os.ReadDir("/var/log") + loginDir := path.Join(targetDir, "login") + if _, err := os.Stat(loginDir); err != nil && os.IsNotExist(err) { + if err = os.MkdirAll(loginDir, os.ModePerm); err != nil { + return err + } + } + if len(loginLogFiles) != 0 { + for i := 0; i < len(loginLogFiles); i++ { + if !loginLogFiles[i].IsDir() && (strings.HasPrefix(loginLogFiles[i].Name(), "secure") || strings.HasPrefix(loginLogFiles[i].Name(), "auth.log")) { + _ = common.CopyFile(path.Join("/var/log", loginLogFiles[i].Name()), loginDir) + } + } + } + global.LOG.Debug("backup ssh log successful!") + + if err := handleTar(targetDir, path.Dir(targetDir), fileName, "", secret); err != nil { + return err + } + defer func() { + _ = os.RemoveAll(targetDir) + }() + return nil +} diff --git a/agent/app/service/cronjob_helper.go b/agent/app/service/cronjob_helper.go new file mode 100644 index 000000000..933bbcae5 --- /dev/null +++ b/agent/app/service/cronjob_helper.go @@ -0,0 +1,393 @@ +package service + +import ( + "context" + "fmt" + "os" + "path" + "strings" + "time" + + "github.com/1Panel-dev/1Panel/agent/buserr" + "github.com/1Panel-dev/1Panel/agent/i18n" + + "github.com/1Panel-dev/1Panel/agent/app/model" + "github.com/1Panel-dev/1Panel/agent/app/repo" + "github.com/1Panel-dev/1Panel/agent/constant" + "github.com/1Panel-dev/1Panel/agent/global" + "github.com/1Panel-dev/1Panel/agent/utils/cloud_storage" + "github.com/1Panel-dev/1Panel/agent/utils/cmd" + "github.com/1Panel-dev/1Panel/agent/utils/files" + "github.com/1Panel-dev/1Panel/agent/utils/ntp" + "github.com/pkg/errors" +) + +func (u *CronjobService) HandleJob(cronjob *model.Cronjob) { + var ( + message []byte + err error + ) + record := cronjobRepo.StartRecords(cronjob.ID, cronjob.KeepLocal, "") + go func() { + switch cronjob.Type { + case "shell": + if len(cronjob.Script) == 0 { + return + } + record.Records = u.generateLogsPath(*cronjob, record.StartTime) + _ = cronjobRepo.UpdateRecords(record.ID, map[string]interface{}{"records": record.Records}) + script := cronjob.Script + if len(cronjob.ContainerName) != 0 { + command := "sh" + if len(cronjob.Command) != 0 { + command = cronjob.Command + } + script = fmt.Sprintf("docker exec %s %s -c \"%s\"", cronjob.ContainerName, command, strings.ReplaceAll(cronjob.Script, "\"", "\\\"")) + } + err = u.handleShell(cronjob.Type, cronjob.Name, script, record.Records) + u.removeExpiredLog(*cronjob) + case "curl": + if len(cronjob.URL) == 0 { + return + } + record.Records = u.generateLogsPath(*cronjob, record.StartTime) + _ = cronjobRepo.UpdateRecords(record.ID, map[string]interface{}{"records": record.Records}) + err = u.handleShell(cronjob.Type, cronjob.Name, fmt.Sprintf("curl '%s'", cronjob.URL), record.Records) + u.removeExpiredLog(*cronjob) + case "ntp": + err = u.handleNtpSync() + u.removeExpiredLog(*cronjob) + case "cutWebsiteLog": + var messageItem []string + messageItem, record.File, err = u.handleCutWebsiteLog(cronjob, record.StartTime) + message = []byte(strings.Join(messageItem, "\n")) + case "clean": + messageItem := "" + messageItem, err = u.handleSystemClean() + message = []byte(messageItem) + u.removeExpiredLog(*cronjob) + case "website": + err = u.handleWebsite(*cronjob, record.StartTime) + case "app": + err = u.handleApp(*cronjob, record.StartTime) + case "database": + err = u.handleDatabase(*cronjob, record.StartTime) + case "directory": + if len(cronjob.SourceDir) == 0 { + return + } + err = u.handleDirectory(*cronjob, record.StartTime) + case "log": + err = u.handleSystemLog(*cronjob, record.StartTime) + case "snapshot": + record.Records = u.generateLogsPath(*cronjob, record.StartTime) + _ = cronjobRepo.UpdateRecords(record.ID, map[string]interface{}{"records": record.Records}) + err = u.handleSnapshot(*cronjob, record.StartTime, record.Records) + } + + if err != nil { + 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 { + record.Records, err = mkdirAndWriteFile(cronjob, record.StartTime, message) + if err != nil { + global.LOG.Errorf("save file %s failed, err: %v", record.Records, err) + } + } + cronjobRepo.EndRecords(record, constant.StatusSuccess, "", record.Records) + }() +} + +func (u *CronjobService) handleShell(cronType, cornName, script, logPath string) error { + handleDir := fmt.Sprintf("%s/task/%s/%s", constant.DataDir, cronType, cornName) + if _, err := os.Stat(handleDir); err != nil && os.IsNotExist(err) { + if err = os.MkdirAll(handleDir, os.ModePerm); err != nil { + return err + } + } + if err := cmd.ExecCronjobWithTimeOut(script, handleDir, logPath, 24*time.Hour); err != nil { + return err + } + return nil +} + +func (u *CronjobService) handleNtpSync() error { + ntpServer, err := settingRepo.Get(settingRepo.WithByKey("NtpSite")) + if err != nil { + return err + } + ntime, err := ntp.GetRemoteTime(ntpServer.Value) + if err != nil { + return err + } + if err := ntp.UpdateSystemTime(ntime.Format(constant.DateTimeLayout)); err != nil { + return err + } + return nil +} + +func handleTar(sourceDir, targetDir, name, exclusionRules string, secret string) error { + if _, err := os.Stat(targetDir); err != nil && os.IsNotExist(err) { + if err = os.MkdirAll(targetDir, os.ModePerm); err != nil { + return err + } + } + + excludes := strings.Split(exclusionRules, ",") + excludeRules := "" + excludes = append(excludes, "*.sock") + for _, exclude := range excludes { + if len(exclude) == 0 { + continue + } + excludeRules += " --exclude " + exclude + } + path := "" + if strings.Contains(sourceDir, "/") { + itemDir := strings.ReplaceAll(sourceDir[strings.LastIndex(sourceDir, "/"):], "/", "") + aheadDir := sourceDir[:strings.LastIndex(sourceDir, "/")] + if len(aheadDir) == 0 { + aheadDir = "/" + } + path += fmt.Sprintf("-C %s %s", aheadDir, itemDir) + } else { + path = sourceDir + } + + commands := "" + + if len(secret) != 0 { + extraCmd := "| openssl enc -aes-256-cbc -salt -k '" + secret + "' -out" + commands = fmt.Sprintf("tar --warning=no-file-changed --ignore-failed-read -zcf %s %s %s %s", " -"+excludeRules, path, extraCmd, targetDir+"/"+name) + global.LOG.Debug(strings.ReplaceAll(commands, fmt.Sprintf(" %s ", secret), "******")) + } else { + commands = fmt.Sprintf("tar --warning=no-file-changed --ignore-failed-read -zcf %s %s %s", targetDir+"/"+name, excludeRules, path) + global.LOG.Debug(commands) + } + stdout, err := cmd.ExecWithTimeOut(commands, 24*time.Hour) + if err != nil { + if len(stdout) != 0 { + global.LOG.Errorf("do handle tar failed, stdout: %s, err: %v", stdout, err) + return fmt.Errorf("do handle tar failed, stdout: %s, err: %v", stdout, err) + } + } + return nil +} + +func handleUnTar(sourceFile, targetDir string, secret string) error { + if _, err := os.Stat(targetDir); err != nil && os.IsNotExist(err) { + if err = os.MkdirAll(targetDir, os.ModePerm); err != nil { + return err + } + } + commands := "" + if len(secret) != 0 { + extraCmd := "openssl enc -d -aes-256-cbc -k '" + secret + "' -in " + sourceFile + " | " + commands = fmt.Sprintf("%s tar -zxvf - -C %s", extraCmd, targetDir+" > /dev/null 2>&1") + global.LOG.Debug(strings.ReplaceAll(commands, fmt.Sprintf(" %s ", secret), "******")) + } else { + commands = fmt.Sprintf("tar zxvfC %s %s", sourceFile, targetDir) + global.LOG.Debug(commands) + } + + stdout, err := cmd.ExecWithTimeOut(commands, 24*time.Hour) + if err != nil { + global.LOG.Errorf("do handle untar failed, stdout: %s, err: %v", stdout, err) + return errors.New(stdout) + } + return nil +} + +func (u *CronjobService) handleCutWebsiteLog(cronjob *model.Cronjob, startTime time.Time) ([]string, string, error) { + var ( + err error + filePaths []string + msgs []string + ) + websites := loadWebsForJob(*cronjob) + nginx, err := getAppInstallByKey(constant.AppOpenresty) + if err != nil { + return msgs, "", nil + } + baseDir := path.Join(nginx.GetPath(), "www", "sites") + fileOp := files.NewFileOp() + for _, website := range websites { + websiteLogDir := path.Join(baseDir, website.Alias, "log") + srcAccessLogPath := path.Join(websiteLogDir, "access.log") + srcErrorLogPath := path.Join(websiteLogDir, "error.log") + dstLogDir := path.Join(global.CONF.System.Backup, "log", "website", website.Alias) + if !fileOp.Stat(dstLogDir) { + _ = os.MkdirAll(dstLogDir, 0755) + } + + dstName := fmt.Sprintf("%s_log_%s.gz", website.PrimaryDomain, startTime.Format(constant.DateTimeSlimLayout)) + dstFilePath := path.Join(dstLogDir, dstName) + filePaths = append(filePaths, dstFilePath) + + if err = backupLogFile(dstFilePath, websiteLogDir, fileOp); err != nil { + websiteErr := buserr.WithNameAndErr("ErrCutWebsiteLog", website.PrimaryDomain, err) + err = websiteErr + msgs = append(msgs, websiteErr.Error()) + global.LOG.Error(websiteErr.Error()) + continue + } else { + _ = fileOp.WriteFile(srcAccessLogPath, strings.NewReader(""), 0755) + _ = fileOp.WriteFile(srcErrorLogPath, strings.NewReader(""), 0755) + } + msg := i18n.GetMsgWithMap("CutWebsiteLogSuccess", map[string]interface{}{"name": website.PrimaryDomain, "path": dstFilePath}) + global.LOG.Infof(msg) + msgs = append(msgs, msg) + } + u.removeExpiredLog(*cronjob) + return msgs, strings.Join(filePaths, ","), err +} + +func backupLogFile(dstFilePath, websiteLogDir string, fileOp files.FileOp) error { + if err := cmd.ExecCmd(fmt.Sprintf("tar -czf %s -C %s %s", dstFilePath, websiteLogDir, strings.Join([]string{"access.log", "error.log"}, " "))); err != nil { + dstDir := path.Dir(dstFilePath) + if err = fileOp.Copy(path.Join(websiteLogDir, "access.log"), dstDir); err != nil { + return err + } + if err = fileOp.Copy(path.Join(websiteLogDir, "error.log"), dstDir); err != nil { + return err + } + if err = cmd.ExecCmd(fmt.Sprintf("tar -czf %s -C %s %s", dstFilePath, dstDir, strings.Join([]string{"access.log", "error.log"}, " "))); err != nil { + return err + } + _ = fileOp.DeleteFile(path.Join(dstDir, "access.log")) + _ = fileOp.DeleteFile(path.Join(dstDir, "error.log")) + return nil + } + return nil +} + +func (u *CronjobService) handleSystemClean() (string, error) { + return NewIDeviceService().CleanForCronjob() +} + +func loadClientMap(backupAccounts string) (map[string]cronjobUploadHelper, error) { + clients := make(map[string]cronjobUploadHelper) + accounts, err := backupRepo.List() + if err != nil { + return nil, err + } + targets := strings.Split(backupAccounts, ",") + for _, target := range targets { + if len(target) == 0 { + continue + } + for _, account := range accounts { + if target == account.Type { + client, err := NewIBackupService().NewClient(&account) + if err != nil { + return nil, err + } + pathItem := account.BackupPath + if account.BackupPath != "/" { + pathItem = strings.TrimPrefix(account.BackupPath, "/") + } + clients[target] = cronjobUploadHelper{ + client: client, + backupPath: pathItem, + backType: account.Type, + } + } + } + } + return clients, nil +} + +type cronjobUploadHelper struct { + backupPath string + backType string + client cloud_storage.CloudStorageClient +} + +func (u *CronjobService) uploadCronjobBackFile(cronjob model.Cronjob, accountMap map[string]cronjobUploadHelper, file string) (string, error) { + defer func() { + _ = os.Remove(file) + }() + accounts := strings.Split(cronjob.BackupAccounts, ",") + cloudSrc := strings.TrimPrefix(file, global.CONF.System.TmpDir+"/") + for _, account := range accounts { + if len(account) != 0 { + global.LOG.Debugf("start upload file to %s, dir: %s", account, path.Join(accountMap[account].backupPath, cloudSrc)) + if _, err := accountMap[account].client.Upload(file, path.Join(accountMap[account].backupPath, cloudSrc)); err != nil { + return "", err + } + global.LOG.Debugf("upload successful!") + } + } + return cloudSrc, nil +} + +func (u *CronjobService) removeExpiredBackup(cronjob model.Cronjob, accountMap map[string]cronjobUploadHelper, record model.BackupRecord) { + global.LOG.Infof("start to handle remove expired, retain copies: %d", cronjob.RetainCopies) + var opts []repo.DBOption + opts = append(opts, commonRepo.WithByFrom("cronjob")) + opts = append(opts, backupRepo.WithByCronID(cronjob.ID)) + opts = append(opts, commonRepo.WithOrderBy("created_at desc")) + if record.ID != 0 { + opts = append(opts, backupRepo.WithByType(record.Type)) + opts = append(opts, commonRepo.WithByName(record.Name)) + opts = append(opts, backupRepo.WithByDetailName(record.DetailName)) + } + records, _ := backupRepo.ListRecord(opts...) + if len(records) <= int(cronjob.RetainCopies) { + return + } + for i := int(cronjob.RetainCopies); i < len(records); i++ { + accounts := strings.Split(cronjob.BackupAccounts, ",") + if cronjob.Type == "snapshot" { + for _, account := range accounts { + if len(account) != 0 { + _, _ = accountMap[account].client.Delete(path.Join(accountMap[account].backupPath, "system_snapshot", records[i].FileName)) + } + } + _ = snapshotRepo.Delete(commonRepo.WithByName(strings.TrimSuffix(records[i].FileName, ".tar.gz"))) + } else { + for _, account := range accounts { + if len(account) != 0 { + _, _ = accountMap[account].client.Delete(path.Join(accountMap[account].backupPath, records[i].FileDir, records[i].FileName)) + } + } + } + _ = backupRepo.DeleteRecord(context.Background(), commonRepo.WithByID(records[i].ID)) + } +} + +func (u *CronjobService) removeExpiredLog(cronjob model.Cronjob) { + global.LOG.Infof("start to handle remove expired, retain copies: %d", cronjob.RetainCopies) + records, _ := cronjobRepo.ListRecord(cronjobRepo.WithByJobID(int(cronjob.ID)), commonRepo.WithOrderBy("created_at desc")) + if len(records) <= int(cronjob.RetainCopies) { + return + } + for i := int(cronjob.RetainCopies); i < len(records); i++ { + if len(records[i].File) != 0 { + files := strings.Split(records[i].File, ",") + for _, file := range files { + _ = os.Remove(file) + } + } + _ = cronjobRepo.DeleteRecord(commonRepo.WithByID(uint(records[i].ID))) + _ = os.Remove(records[i].Records) + } +} + +func (u *CronjobService) generateLogsPath(cronjob model.Cronjob, startTime time.Time) string { + dir := fmt.Sprintf("%s/task/%s/%s", constant.DataDir, cronjob.Type, cronjob.Name) + if _, err := os.Stat(dir); err != nil && os.IsNotExist(err) { + _ = os.MkdirAll(dir, os.ModePerm) + } + + path := fmt.Sprintf("%s/%s.log", dir, startTime.Format(constant.DateTimeSlimLayout)) + return path +} + +func hasBackup(cronjobType string) bool { + return cronjobType == "app" || cronjobType == "database" || cronjobType == "website" || cronjobType == "directory" || cronjobType == "snapshot" || cronjobType == "log" +} diff --git a/agent/app/service/dashboard.go b/agent/app/service/dashboard.go new file mode 100644 index 000000000..9558e1ca1 --- /dev/null +++ b/agent/app/service/dashboard.go @@ -0,0 +1,325 @@ +package service + +import ( + "encoding/json" + "fmt" + "sort" + "strings" + "sync" + "time" + + "github.com/1Panel-dev/1Panel/agent/app/dto" + "github.com/1Panel-dev/1Panel/agent/constant" + "github.com/1Panel-dev/1Panel/agent/global" + "github.com/1Panel-dev/1Panel/agent/utils/cmd" + "github.com/1Panel-dev/1Panel/agent/utils/copier" + "github.com/1Panel-dev/1Panel/agent/utils/xpack" + "github.com/shirou/gopsutil/v3/cpu" + "github.com/shirou/gopsutil/v3/disk" + "github.com/shirou/gopsutil/v3/host" + "github.com/shirou/gopsutil/v3/load" + "github.com/shirou/gopsutil/v3/mem" + "github.com/shirou/gopsutil/v3/net" +) + +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 + + Restart(operation string) error +} + +func NewIDashboardService() IDashboardService { + return &DashboardService{} +} + +func (u *DashboardService) Restart(operation string) error { + if operation != "1panel" && operation != "system" { + return fmt.Errorf("handle restart operation %s failed, err: nonsupport such operation", operation) + } + itemCmd := fmt.Sprintf("%s 1pctl restart", cmd.SudoHandleCmd()) + if operation == "system" { + itemCmd = fmt.Sprintf("%s reboot", cmd.SudoHandleCmd()) + } + go func() { + stdout, err := cmd.Exec(itemCmd) + if err != nil { + global.LOG.Errorf("handle %s failed, err: %v", itemCmd, stdout) + } + }() + 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 + + diskInfo, err := disk.Usage(global.CONF.System.BaseDir) + if err == nil { + baseInfo.DiskSize = int64(diskInfo.Free) + } + + 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() + if err != nil { + return nil, err + } + baseInfo.Hostname = hostInfo.Hostname + baseInfo.OS = hostInfo.OS + baseInfo.Platform = hostInfo.Platform + baseInfo.PlatformFamily = hostInfo.PlatformFamily + baseInfo.PlatformVersion = hostInfo.PlatformVersion + baseInfo.KernelArch = hostInfo.KernelArch + baseInfo.KernelVersion = hostInfo.KernelVersion + ss, _ := json.Marshal(hostInfo) + baseInfo.VirtualizationSystem = string(ss) + + appInstall, err := appInstallRepo.ListBy() + if err != nil { + return nil, err + } + baseInfo.AppInstalledNumber = len(appInstall) + postgresqlDbs, err := postgresqlRepo.List() + if err != nil { + return nil, err + } + mysqlDbs, err := mysqlRepo.List() + if err != nil { + return nil, err + } + baseInfo.DatabaseNumber = len(mysqlDbs) + len(postgresqlDbs) + website, err := websiteRepo.GetBy() + if err != nil { + return nil, err + } + baseInfo.WebsiteNumber = len(website) + cronjobs, err := cronjobRepo.List() + if err != nil { + return nil, err + } + baseInfo.CronjobNumber = len(cronjobs) + + cpuInfo, err := cpu.Info() + if err == nil { + baseInfo.CPUModelName = cpuInfo[0].ModelName + } + + baseInfo.CPUCores, _ = cpu.Counts(false) + baseInfo.CPULogicalCores, _ = cpu.Counts(true) + + baseInfo.CurrentInfo = *u.LoadCurrentInfo(ioOption, netOption) + return &baseInfo, nil +} + +func (u *DashboardService) LoadCurrentInfo(ioOption string, netOption string) *dto.DashboardCurrent { + var currentInfo dto.DashboardCurrent + hostInfo, _ := host.Info() + currentInfo.Uptime = hostInfo.Uptime + currentInfo.TimeSinceUptime = time.Now().Add(-time.Duration(hostInfo.Uptime) * time.Second).Format(constant.DateTimeLayout) + currentInfo.Procs = hostInfo.Procs + + currentInfo.CPUTotal, _ = cpu.Counts(true) + totalPercent, _ := cpu.Percent(0, false) + if len(totalPercent) == 1 { + currentInfo.CPUUsedPercent = totalPercent[0] + currentInfo.CPUUsed = currentInfo.CPUUsedPercent * 0.01 * float64(currentInfo.CPUTotal) + } + currentInfo.CPUPercent, _ = cpu.Percent(0, true) + + loadInfo, _ := load.Avg() + currentInfo.Load1 = loadInfo.Load1 + currentInfo.Load5 = loadInfo.Load5 + currentInfo.Load15 = loadInfo.Load15 + currentInfo.LoadUsagePercent = loadInfo.Load1 / (float64(currentInfo.CPUTotal*2) * 0.75) * 100 + + memoryInfo, _ := mem.VirtualMemory() + currentInfo.MemoryTotal = memoryInfo.Total + currentInfo.MemoryAvailable = memoryInfo.Available + currentInfo.MemoryUsed = memoryInfo.Used + currentInfo.MemoryUsedPercent = memoryInfo.UsedPercent + + swapInfo, _ := mem.SwapMemory() + currentInfo.SwapMemoryTotal = swapInfo.Total + currentInfo.SwapMemoryAvailable = swapInfo.Free + currentInfo.SwapMemoryUsed = swapInfo.Used + currentInfo.SwapMemoryUsedPercent = swapInfo.UsedPercent + + currentInfo.DiskData = loadDiskInfo() + currentInfo.GPUData = loadGPUInfo() + + if ioOption == "all" { + diskInfo, _ := disk.IOCounters() + for _, state := range diskInfo { + currentInfo.IOReadBytes += state.ReadBytes + currentInfo.IOWriteBytes += state.WriteBytes + currentInfo.IOCount += (state.ReadCount + state.WriteCount) + currentInfo.IOReadTime += state.ReadTime + currentInfo.IOWriteTime += state.WriteTime + } + } else { + diskInfo, _ := disk.IOCounters(ioOption) + for _, state := range diskInfo { + currentInfo.IOReadBytes += state.ReadBytes + currentInfo.IOWriteBytes += state.WriteBytes + currentInfo.IOCount += (state.ReadCount + state.WriteCount) + currentInfo.IOReadTime += state.ReadTime + currentInfo.IOWriteTime += state.WriteTime + } + } + + if netOption == "all" { + netInfo, _ := net.IOCounters(false) + if len(netInfo) != 0 { + currentInfo.NetBytesSent = netInfo[0].BytesSent + currentInfo.NetBytesRecv = netInfo[0].BytesRecv + } + } else { + netInfo, _ := net.IOCounters(true) + for _, state := range netInfo { + if state.Name == netOption { + currentInfo.NetBytesSent = state.BytesSent + currentInfo.NetBytesRecv = state.BytesRecv + } + } + } + + currentInfo.ShotTime = time.Now() + return ¤tInfo +} + +type diskInfo struct { + Type string + Mount string + Device string +} + +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 { + 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") + + var mounts []diskInfo + var excludes = []string{"/mnt/cdrom", "/boot", "/boot/efi", "/dev", "/dev/shm", "/run/lock", "/run", "/run/shm", "/run/user"} + for _, line := range lines { + fields := strings.Fields(line) + if len(fields) < 7 { + continue + } + if strings.HasPrefix(fields[6], "/snap") || len(strings.Split(fields[6], "/")) > 10 { + continue + } + if strings.TrimSpace(fields[1]) == "tmpfs" { + continue + } + if strings.Contains(fields[2], "K") { + continue + } + if strings.Contains(fields[6], "docker") { + continue + } + isExclude := false + for _, exclude := range excludes { + if exclude == fields[6] { + isExclude = true + } + } + if isExclude { + continue + } + mounts = append(mounts, diskInfo{Type: fields[1], Device: fields[0], Mount: strings.Join(fields[6:], " ")}) + } + + var ( + wg sync.WaitGroup + mu sync.Mutex + ) + wg.Add(len(mounts)) + for i := 0; i < len(mounts); i++ { + go func(timeoutCh <-chan time.Time, mount diskInfo) { + defer wg.Done() + + var itemData dto.DiskInfo + itemData.Path = mount.Mount + itemData.Type = mount.Type + itemData.Device = mount.Device + select { + case <-timeoutCh: + mu.Lock() + datas = append(datas, itemData) + mu.Unlock() + global.LOG.Errorf("load disk info from %s failed, err: timeout", mount.Mount) + default: + state, err := disk.Usage(mount.Mount) + if err != nil { + mu.Lock() + datas = append(datas, itemData) + mu.Unlock() + global.LOG.Errorf("load disk info from %s failed, err: %v", mount.Mount, err) + return + } + itemData.Total = state.Total + itemData.Free = state.Free + itemData.Used = state.Used + itemData.UsedPercent = state.UsedPercent + itemData.InodesTotal = state.InodesTotal + itemData.InodesUsed = state.InodesUsed + itemData.InodesFree = state.InodesFree + itemData.InodesUsedPercent = state.InodesUsedPercent + mu.Lock() + datas = append(datas, itemData) + mu.Unlock() + } + }(time.After(5*time.Second), mounts[i]) + } + wg.Wait() + + sort.Slice(datas, func(i, j int) bool { + return datas[i].Path < datas[j].Path + }) + return datas +} + +func loadGPUInfo() []dto.GPUInfo { + list := xpack.LoadGpuInfo() + if len(list) == 0 { + return nil + } + var data []dto.GPUInfo + for _, gpu := range list { + var dataItem dto.GPUInfo + if err := copier.Copy(&dataItem, &gpu); err != nil { + continue + } + dataItem.PowerUsage = dataItem.PowerDraw + " / " + dataItem.MaxPowerLimit + dataItem.MemoryUsage = dataItem.MemUsed + " / " + dataItem.MemTotal + data = append(data, dataItem) + } + return data +} diff --git a/agent/app/service/database.go b/agent/app/service/database.go new file mode 100644 index 000000000..74e4e2c6c --- /dev/null +++ b/agent/app/service/database.go @@ -0,0 +1,330 @@ +package service + +import ( + "context" + "fmt" + "os" + "path" + + "github.com/1Panel-dev/1Panel/agent/utils/postgresql" + pgclient "github.com/1Panel-dev/1Panel/agent/utils/postgresql/client" + redisclient "github.com/1Panel-dev/1Panel/agent/utils/redis" + + "github.com/1Panel-dev/1Panel/agent/app/dto" + "github.com/1Panel-dev/1Panel/agent/buserr" + "github.com/1Panel-dev/1Panel/agent/constant" + "github.com/1Panel-dev/1Panel/agent/global" + "github.com/1Panel-dev/1Panel/agent/utils/encrypt" + "github.com/1Panel-dev/1Panel/agent/utils/mysql" + "github.com/1Panel-dev/1Panel/agent/utils/mysql/client" + "github.com/jinzhu/copier" + "github.com/pkg/errors" +) + +type DatabaseService struct{} + +type IDatabaseService interface { + Get(name string) (dto.DatabaseInfo, error) + SearchWithPage(search dto.DatabaseSearch) (int64, interface{}, error) + CheckDatabase(req dto.DatabaseCreate) bool + Create(req dto.DatabaseCreate) error + Update(req dto.DatabaseUpdate) error + DeleteCheck(id uint) ([]string, error) + Delete(req dto.DatabaseDelete) error + List(dbType string) ([]dto.DatabaseOption, error) + LoadItems(dbType string) ([]dto.DatabaseItem, error) +} + +func NewIDatabaseService() IDatabaseService { + return &DatabaseService{} +} + +func (u *DatabaseService) SearchWithPage(search dto.DatabaseSearch) (int64, interface{}, error) { + total, dbs, err := databaseRepo.Page(search.Page, search.PageSize, + databaseRepo.WithTypeList(search.Type), + commonRepo.WithLikeName(search.Info), + commonRepo.WithOrderRuleBy(search.OrderBy, search.Order), + databaseRepo.WithoutByFrom("local"), + ) + var datas []dto.DatabaseInfo + for _, db := range dbs { + var item dto.DatabaseInfo + if err := copier.Copy(&item, &db); err != nil { + return 0, nil, errors.WithMessage(constant.ErrStructTransform, err.Error()) + } + datas = append(datas, item) + } + return total, datas, err +} + +func (u *DatabaseService) Get(name string) (dto.DatabaseInfo, error) { + var data dto.DatabaseInfo + remote, err := databaseRepo.Get(commonRepo.WithByName(name)) + if err != nil { + return data, err + } + if err := copier.Copy(&data, &remote); err != nil { + return data, errors.WithMessage(constant.ErrStructTransform, err.Error()) + } + return data, nil +} + +func (u *DatabaseService) List(dbType string) ([]dto.DatabaseOption, error) { + dbs, err := databaseRepo.GetList(databaseRepo.WithTypeList(dbType)) + if err != nil { + return nil, err + } + var datas []dto.DatabaseOption + for _, db := range dbs { + var item dto.DatabaseOption + if err := copier.Copy(&item, &db); err != nil { + return nil, errors.WithMessage(constant.ErrStructTransform, err.Error()) + } + item.Database = db.Name + datas = append(datas, item) + } + return datas, err +} + +func (u *DatabaseService) LoadItems(dbType string) ([]dto.DatabaseItem, error) { + dbs, err := databaseRepo.GetList(databaseRepo.WithTypeList(dbType)) + var datas []dto.DatabaseItem + for _, db := range dbs { + if dbType == "postgresql" { + items, _ := postgresqlRepo.List(postgresqlRepo.WithByPostgresqlName(db.Name)) + for _, item := range items { + var dItem dto.DatabaseItem + if err := copier.Copy(&dItem, &item); err != nil { + continue + } + dItem.Database = db.Name + datas = append(datas, dItem) + } + } else { + items, _ := mysqlRepo.List(mysqlRepo.WithByMysqlName(db.Name)) + for _, item := range items { + var dItem dto.DatabaseItem + if err := copier.Copy(&dItem, &item); err != nil { + continue + } + dItem.Database = db.Name + datas = append(datas, dItem) + } + } + } + return datas, err +} + +func (u *DatabaseService) CheckDatabase(req dto.DatabaseCreate) bool { + switch req.Type { + case constant.AppPostgresql: + _, err := postgresql.NewPostgresqlClient(pgclient.DBInfo{ + From: "remote", + Address: req.Address, + Port: req.Port, + Username: req.Username, + Password: req.Password, + Timeout: 6, + }) + return err == nil + case constant.AppRedis: + _, err := redisclient.NewRedisClient(redisclient.DBInfo{ + Address: req.Address, + Port: req.Port, + Password: req.Password, + }) + return err == nil + case "mysql", "mariadb": + _, err := mysql.NewMysqlClient(client.DBInfo{ + From: "remote", + Address: req.Address, + Port: req.Port, + Username: req.Username, + Password: req.Password, + + SSL: req.SSL, + RootCert: req.RootCert, + ClientKey: req.ClientKey, + ClientCert: req.ClientCert, + SkipVerify: req.SkipVerify, + Timeout: 6, + }) + return err == nil + } + + return false +} + +func (u *DatabaseService) Create(req dto.DatabaseCreate) error { + db, _ := databaseRepo.Get(commonRepo.WithByName(req.Name)) + if db.ID != 0 { + if db.From == "local" { + return buserr.New(constant.ErrLocalExist) + } + return constant.ErrRecordExist + } + switch req.Type { + case constant.AppPostgresql: + if _, err := postgresql.NewPostgresqlClient(pgclient.DBInfo{ + From: "remote", + Address: req.Address, + Port: req.Port, + Username: req.Username, + Password: req.Password, + Timeout: 6, + }); err != nil { + return err + } + case constant.AppRedis: + if _, err := redisclient.NewRedisClient(redisclient.DBInfo{ + Address: req.Address, + Port: req.Port, + Password: req.Password, + }); err != nil { + return err + } + case "mysql", "mariadb": + if _, err := mysql.NewMysqlClient(client.DBInfo{ + From: "remote", + Address: req.Address, + Port: req.Port, + Username: req.Username, + Password: req.Password, + + SSL: req.SSL, + RootCert: req.RootCert, + ClientKey: req.ClientKey, + ClientCert: req.ClientCert, + SkipVerify: req.SkipVerify, + Timeout: 6, + }); err != nil { + return err + } + default: + return errors.New("database type not supported") + } + + if err := copier.Copy(&db, &req); err != nil { + return errors.WithMessage(constant.ErrStructTransform, err.Error()) + } + if err := databaseRepo.Create(context.Background(), &db); err != nil { + return err + } + return nil +} + +func (u *DatabaseService) DeleteCheck(id uint) ([]string, error) { + var appInUsed []string + apps, _ := appInstallResourceRepo.GetBy(databaseRepo.WithByFrom("remote"), appInstallResourceRepo.WithLinkId(id)) + for _, app := range apps { + appInstall, _ := appInstallRepo.GetFirst(commonRepo.WithByID(app.AppInstallId)) + if appInstall.ID != 0 { + appInUsed = append(appInUsed, appInstall.Name) + } + } + + return appInUsed, nil +} + +func (u *DatabaseService) Delete(req dto.DatabaseDelete) error { + db, _ := databaseRepo.Get(commonRepo.WithByID(req.ID)) + if db.ID == 0 { + return constant.ErrRecordNotFound + } + + if req.DeleteBackup { + uploadDir := path.Join(global.CONF.System.BaseDir, fmt.Sprintf("1panel/uploads/database/%s/%s", db.Type, db.Name)) + if _, err := os.Stat(uploadDir); err == nil { + _ = os.RemoveAll(uploadDir) + } + localDir, err := loadLocalDir() + if err != nil && !req.ForceDelete { + return err + } + backupDir := path.Join(localDir, fmt.Sprintf("database/%s/%s", db.Type, db.Name)) + if _, err := os.Stat(backupDir); err == nil { + _ = os.RemoveAll(backupDir) + } + _ = backupRepo.DeleteRecord(context.Background(), commonRepo.WithByType(db.Type), commonRepo.WithByName(db.Name)) + global.LOG.Infof("delete database %s-%s backups successful", db.Type, db.Name) + } + + if err := databaseRepo.Delete(context.Background(), commonRepo.WithByID(req.ID)); err != nil && !req.ForceDelete { + return err + } + if db.From != "local" { + if db.Type == "mysql" || db.Type == "mariadb" { + if err := mysqlRepo.Delete(context.Background(), mysqlRepo.WithByMysqlName(db.Name)); err != nil && !req.ForceDelete { + return err + } + } else { + if err := postgresqlRepo.Delete(context.Background(), postgresqlRepo.WithByPostgresqlName(db.Name)); err != nil && !req.ForceDelete { + return err + } + } + } + return nil +} + +func (u *DatabaseService) Update(req dto.DatabaseUpdate) error { + switch req.Type { + case constant.AppPostgresql: + if _, err := postgresql.NewPostgresqlClient(pgclient.DBInfo{ + From: "remote", + Address: req.Address, + Port: req.Port, + Username: req.Username, + Password: req.Password, + Timeout: 300, + }); err != nil { + return err + } + case constant.AppRedis: + if _, err := redisclient.NewRedisClient(redisclient.DBInfo{ + Address: req.Address, + Port: req.Port, + Password: req.Password, + }); err != nil { + return err + } + case "mysql", "mariadb": + if _, err := mysql.NewMysqlClient(client.DBInfo{ + From: "remote", + Address: req.Address, + Port: req.Port, + Username: req.Username, + Password: req.Password, + + SSL: req.SSL, + RootCert: req.RootCert, + ClientKey: req.ClientKey, + ClientCert: req.ClientCert, + SkipVerify: req.SkipVerify, + Timeout: 300, + }); err != nil { + return err + } + default: + return errors.New("database type not supported") + } + + pass, err := encrypt.StringEncrypt(req.Password) + if err != nil { + return fmt.Errorf("decrypt database password failed, err: %v", err) + } + + upMap := make(map[string]interface{}) + upMap["type"] = req.Type + upMap["version"] = req.Version + upMap["address"] = req.Address + upMap["port"] = req.Port + upMap["username"] = req.Username + upMap["password"] = pass + upMap["description"] = req.Description + upMap["ssl"] = req.SSL + upMap["client_key"] = req.ClientKey + upMap["client_cert"] = req.ClientCert + upMap["root_cert"] = req.RootCert + upMap["skip_verify"] = req.SkipVerify + return databaseRepo.Update(req.ID, upMap) +} diff --git a/agent/app/service/database_common.go b/agent/app/service/database_common.go new file mode 100644 index 000000000..6b7389714 --- /dev/null +++ b/agent/app/service/database_common.go @@ -0,0 +1,94 @@ +package service + +import ( + "bufio" + "fmt" + "os" + "path" + + "github.com/1Panel-dev/1Panel/agent/app/dto" + "github.com/1Panel-dev/1Panel/agent/buserr" + "github.com/1Panel-dev/1Panel/agent/constant" + "github.com/1Panel-dev/1Panel/agent/global" + "github.com/1Panel-dev/1Panel/agent/utils/compose" +) + +type DBCommonService struct{} + +type IDBCommonService interface { + LoadBaseInfo(req dto.OperationWithNameAndType) (*dto.DBBaseInfo, error) + LoadDatabaseFile(req dto.OperationWithNameAndType) (string, error) + + UpdateConfByFile(req dto.DBConfUpdateByFile) error +} + +func NewIDBCommonService() IDBCommonService { + return &DBCommonService{} +} + +func (u *DBCommonService) LoadBaseInfo(req dto.OperationWithNameAndType) (*dto.DBBaseInfo, error) { + var data dto.DBBaseInfo + app, err := appInstallRepo.LoadBaseInfo(req.Type, req.Name) + if err != nil { + return nil, err + } + data.ContainerName = app.ContainerName + data.Name = app.Name + data.Port = int64(app.Port) + + return &data, nil +} + +func (u *DBCommonService) LoadDatabaseFile(req dto.OperationWithNameAndType) (string, error) { + filePath := "" + switch req.Type { + case "mysql-conf": + filePath = path.Join(global.CONF.System.DataDir, fmt.Sprintf("apps/mysql/%s/conf/my.cnf", req.Name)) + case "mariadb-conf": + filePath = path.Join(global.CONF.System.DataDir, fmt.Sprintf("apps/mariadb/%s/conf/my.cnf", req.Name)) + case "postgresql-conf": + filePath = path.Join(global.CONF.System.DataDir, fmt.Sprintf("apps/postgresql/%s/data/postgresql.conf", req.Name)) + case "redis-conf": + filePath = path.Join(global.CONF.System.DataDir, fmt.Sprintf("apps/redis/%s/conf/redis.conf", req.Name)) + case "mysql-slow-logs": + filePath = path.Join(global.CONF.System.DataDir, fmt.Sprintf("apps/mysql/%s/data/1Panel-slow.log", req.Name)) + case "mariadb-slow-logs": + filePath = path.Join(global.CONF.System.DataDir, fmt.Sprintf("apps/mariadb/%s/db/data/1Panel-slow.log", req.Name)) + } + if _, err := os.Stat(filePath); err != nil { + return "", buserr.New("ErrHttpReqNotFound") + } + content, err := os.ReadFile(filePath) + if err != nil { + return "", err + } + return string(content), nil +} + +func (u *DBCommonService) UpdateConfByFile(req dto.DBConfUpdateByFile) error { + app, err := appInstallRepo.LoadBaseInfo(req.Type, req.Database) + if err != nil { + return err + } + path := "" + switch req.Type { + case constant.AppMariaDB, constant.AppMysql: + path = fmt.Sprintf("%s/%s/%s/conf/my.cnf", constant.AppInstallDir, req.Type, app.Name) + case constant.AppPostgresql: + path = fmt.Sprintf("%s/%s/%s/data/postgresql.conf", constant.AppInstallDir, req.Type, app.Name) + case constant.AppRedis: + path = fmt.Sprintf("%s/%s/%s/conf/redis.conf", constant.AppInstallDir, req.Type, app.Name) + } + file, err := os.OpenFile(path, os.O_WRONLY|os.O_TRUNC, 0640) + if err != nil { + return err + } + defer file.Close() + write := bufio.NewWriter(file) + _, _ = write.WriteString(req.File) + write.Flush() + if _, err := compose.Restart(fmt.Sprintf("%s/%s/%s/docker-compose.yml", constant.AppInstallDir, req.Type, app.Name)); err != nil { + return err + } + return nil +} diff --git a/agent/app/service/database_mysql.go b/agent/app/service/database_mysql.go new file mode 100644 index 000000000..c88ad00b1 --- /dev/null +++ b/agent/app/service/database_mysql.go @@ -0,0 +1,662 @@ +package service + +import ( + "context" + "encoding/json" + "fmt" + "os" + "os/exec" + "path" + "regexp" + "strconv" + "strings" + "time" + + "github.com/1Panel-dev/1Panel/agent/app/dto" + "github.com/1Panel-dev/1Panel/agent/app/model" + "github.com/1Panel-dev/1Panel/agent/buserr" + "github.com/1Panel-dev/1Panel/agent/constant" + "github.com/1Panel-dev/1Panel/agent/global" + "github.com/1Panel-dev/1Panel/agent/utils/cmd" + "github.com/1Panel-dev/1Panel/agent/utils/common" + "github.com/1Panel-dev/1Panel/agent/utils/compose" + "github.com/1Panel-dev/1Panel/agent/utils/encrypt" + "github.com/1Panel-dev/1Panel/agent/utils/mysql" + "github.com/1Panel-dev/1Panel/agent/utils/mysql/client" + _ "github.com/go-sql-driver/mysql" + "github.com/jinzhu/copier" + "github.com/pkg/errors" +) + +type MysqlService struct{} + +type IMysqlService interface { + SearchWithPage(search dto.MysqlDBSearch) (int64, interface{}, error) + ListDBOption() ([]dto.MysqlOption, error) + Create(ctx context.Context, req dto.MysqlDBCreate) (*model.DatabaseMysql, error) + BindUser(req dto.BindUser) error + LoadFromRemote(req dto.MysqlLoadDB) error + ChangeAccess(info dto.ChangeDBInfo) error + ChangePassword(info dto.ChangeDBInfo) error + UpdateVariables(req dto.MysqlVariablesUpdate) error + UpdateDescription(req dto.UpdateDescription) error + DeleteCheck(req dto.MysqlDBDeleteCheck) ([]string, error) + Delete(ctx context.Context, req dto.MysqlDBDelete) error + + LoadStatus(req dto.OperationWithNameAndType) (*dto.MysqlStatus, error) + LoadVariables(req dto.OperationWithNameAndType) (*dto.MysqlVariables, error) + LoadRemoteAccess(req dto.OperationWithNameAndType) (bool, error) +} + +func NewIMysqlService() IMysqlService { + return &MysqlService{} +} + +func (u *MysqlService) SearchWithPage(search dto.MysqlDBSearch) (int64, interface{}, error) { + total, mysqls, err := mysqlRepo.Page(search.Page, search.PageSize, + mysqlRepo.WithByMysqlName(search.Database), + commonRepo.WithLikeName(search.Info), + commonRepo.WithOrderRuleBy(search.OrderBy, search.Order), + ) + var dtoMysqls []dto.MysqlDBInfo + for _, mysql := range mysqls { + var item dto.MysqlDBInfo + if err := copier.Copy(&item, &mysql); err != nil { + return 0, nil, errors.WithMessage(constant.ErrStructTransform, err.Error()) + } + dtoMysqls = append(dtoMysqls, item) + } + return total, dtoMysqls, err +} + +func (u *MysqlService) ListDBOption() ([]dto.MysqlOption, error) { + mysqls, err := mysqlRepo.List() + if err != nil { + return nil, err + } + + databases, err := databaseRepo.GetList(databaseRepo.WithTypeList("mysql,mariadb")) + if err != nil { + return nil, err + } + var dbs []dto.MysqlOption + for _, mysql := range mysqls { + var item dto.MysqlOption + if err := copier.Copy(&item, &mysql); err != nil { + return nil, errors.WithMessage(constant.ErrStructTransform, err.Error()) + } + item.Database = mysql.MysqlName + for _, database := range databases { + if database.Name == item.Database { + item.Type = database.Type + } + } + dbs = append(dbs, item) + } + return dbs, err +} + +func (u *MysqlService) Create(ctx context.Context, req dto.MysqlDBCreate) (*model.DatabaseMysql, error) { + if cmd.CheckIllegal(req.Name, req.Username, req.Password, req.Format, req.Permission) { + return nil, buserr.New(constant.ErrCmdIllegal) + } + + mysql, _ := mysqlRepo.Get(commonRepo.WithByName(req.Name), mysqlRepo.WithByMysqlName(req.Database), databaseRepo.WithByFrom(req.From)) + if mysql.ID != 0 { + return nil, constant.ErrRecordExist + } + + var createItem model.DatabaseMysql + if err := copier.Copy(&createItem, &req); err != nil { + return nil, errors.WithMessage(constant.ErrStructTransform, err.Error()) + } + + if req.From == "local" && req.Username == "root" { + return nil, errors.New("Cannot set root as user name") + } + + cli, version, err := LoadMysqlClientByFrom(req.Database) + if err != nil { + return nil, err + } + createItem.MysqlName = req.Database + defer cli.Close() + if err := cli.Create(client.CreateInfo{ + Name: req.Name, + Format: req.Format, + Username: req.Username, + Password: req.Password, + Permission: req.Permission, + Version: version, + Timeout: 300, + }); err != nil { + return nil, err + } + + global.LOG.Infof("create database %s successful!", req.Name) + if err := mysqlRepo.Create(ctx, &createItem); err != nil { + return nil, err + } + return &createItem, nil +} + +func (u *MysqlService) BindUser(req dto.BindUser) error { + dbItem, err := mysqlRepo.Get(mysqlRepo.WithByMysqlName(req.Database), commonRepo.WithByName(req.DB)) + if err != nil { + return err + } + cli, version, err := LoadMysqlClientByFrom(req.Database) + if err != nil { + return err + } + defer cli.Close() + + if err := cli.CreateUser(client.CreateInfo{ + Name: dbItem.Name, + Format: dbItem.Format, + Username: req.Username, + Password: req.Password, + Permission: req.Permission, + Version: version, + Timeout: 300, + }, false); err != nil { + return err + } + pass, err := encrypt.StringEncrypt(req.Password) + if err != nil { + return fmt.Errorf("decrypt database db password failed, err: %v", err) + } + if err := mysqlRepo.Update(dbItem.ID, map[string]interface{}{ + "username": req.Username, + "password": pass, + "permission": req.Permission, + }); err != nil { + return err + } + return nil +} + +func (u *MysqlService) LoadFromRemote(req dto.MysqlLoadDB) error { + client, version, err := LoadMysqlClientByFrom(req.Database) + if err != nil { + return err + } + + databases, err := mysqlRepo.List(mysqlRepo.WithByMysqlName(req.Database)) + if err != nil { + return err + } + datas, err := client.SyncDB(version) + if err != nil { + return err + } + deleteList := databases + for _, data := range datas { + hasOld := false + for i := 0; i < len(databases); i++ { + if strings.EqualFold(databases[i].Name, data.Name) && strings.EqualFold(databases[i].MysqlName, data.MysqlName) { + hasOld = true + if databases[i].IsDelete { + _ = mysqlRepo.Update(databases[i].ID, map[string]interface{}{"is_delete": false}) + } + deleteList = append(deleteList[:i], deleteList[i+1:]...) + break + } + } + if !hasOld { + var createItem model.DatabaseMysql + if err := copier.Copy(&createItem, &data); err != nil { + return errors.WithMessage(constant.ErrStructTransform, err.Error()) + } + if err := mysqlRepo.Create(context.Background(), &createItem); err != nil { + return err + } + } + } + for _, delItem := range deleteList { + _ = mysqlRepo.Update(delItem.ID, map[string]interface{}{"is_delete": true}) + } + return nil +} + +func (u *MysqlService) UpdateDescription(req dto.UpdateDescription) error { + return mysqlRepo.Update(req.ID, map[string]interface{}{"description": req.Description}) +} + +func (u *MysqlService) DeleteCheck(req dto.MysqlDBDeleteCheck) ([]string, error) { + var appInUsed []string + db, err := mysqlRepo.Get(commonRepo.WithByID(req.ID)) + if err != nil { + return appInUsed, err + } + + if db.From == "local" { + app, err := appInstallRepo.LoadBaseInfo(req.Type, req.Database) + if err != nil { + return appInUsed, err + } + apps, _ := appInstallResourceRepo.GetBy(appInstallResourceRepo.WithLinkId(app.ID), appInstallResourceRepo.WithResourceId(db.ID)) + for _, app := range apps { + appInstall, _ := appInstallRepo.GetFirst(commonRepo.WithByID(app.AppInstallId)) + if appInstall.ID != 0 { + appInUsed = append(appInUsed, appInstall.Name) + } + } + } else { + apps, _ := appInstallResourceRepo.GetBy(appInstallResourceRepo.WithResourceId(db.ID), appRepo.WithKey(req.Type)) + for _, app := range apps { + appInstall, _ := appInstallRepo.GetFirst(commonRepo.WithByID(app.AppInstallId)) + if appInstall.ID != 0 { + appInUsed = append(appInUsed, appInstall.Name) + } + } + } + + return appInUsed, nil +} + +func (u *MysqlService) Delete(ctx context.Context, req dto.MysqlDBDelete) error { + db, err := mysqlRepo.Get(commonRepo.WithByID(req.ID)) + if err != nil && !req.ForceDelete { + return err + } + cli, version, err := LoadMysqlClientByFrom(req.Database) + if err != nil { + return err + } + defer cli.Close() + if err := cli.Delete(client.DeleteInfo{ + Name: db.Name, + Version: version, + Username: db.Username, + Permission: db.Permission, + Timeout: 300, + }); err != nil && !req.ForceDelete { + return err + } + + if req.DeleteBackup { + uploadDir := path.Join(global.CONF.System.BaseDir, fmt.Sprintf("1panel/uploads/database/%s/%s/%s", req.Type, req.Database, db.Name)) + if _, err := os.Stat(uploadDir); err == nil { + _ = os.RemoveAll(uploadDir) + } + localDir, err := loadLocalDir() + if err != nil && !req.ForceDelete { + return err + } + backupDir := path.Join(localDir, fmt.Sprintf("database/%s/%s/%s", req.Type, db.MysqlName, db.Name)) + if _, err := os.Stat(backupDir); err == nil { + _ = os.RemoveAll(backupDir) + } + _ = backupRepo.DeleteRecord(ctx, commonRepo.WithByType(req.Type), commonRepo.WithByName(req.Database), backupRepo.WithByDetailName(db.Name)) + global.LOG.Infof("delete database %s-%s backups successful", req.Database, db.Name) + } + + _ = mysqlRepo.Delete(ctx, commonRepo.WithByID(db.ID)) + return nil +} + +func (u *MysqlService) ChangePassword(req dto.ChangeDBInfo) error { + if cmd.CheckIllegal(req.Value) { + return buserr.New(constant.ErrCmdIllegal) + } + cli, version, err := LoadMysqlClientByFrom(req.Database) + if err != nil { + return err + } + defer cli.Close() + var ( + mysqlData model.DatabaseMysql + passwordInfo client.PasswordChangeInfo + ) + passwordInfo.Password = req.Value + passwordInfo.Timeout = 300 + passwordInfo.Version = version + + if req.ID != 0 { + mysqlData, err = mysqlRepo.Get(commonRepo.WithByID(req.ID)) + if err != nil { + return err + } + passwordInfo.Name = mysqlData.Name + passwordInfo.Username = mysqlData.Username + passwordInfo.Permission = mysqlData.Permission + } else { + passwordInfo.Username = "root" + } + if err := cli.ChangePassword(passwordInfo); err != nil { + return err + } + + if req.ID != 0 { + var appRess []model.AppInstallResource + if req.From == "local" { + app, err := appInstallRepo.LoadBaseInfo(req.Type, req.Database) + if err != nil { + return err + } + appRess, _ = appInstallResourceRepo.GetBy(appInstallResourceRepo.WithLinkId(app.ID), appInstallResourceRepo.WithResourceId(mysqlData.ID)) + } else { + appRess, _ = appInstallResourceRepo.GetBy(appInstallResourceRepo.WithResourceId(mysqlData.ID)) + } + for _, appRes := range appRess { + appInstall, err := appInstallRepo.GetFirst(commonRepo.WithByID(appRes.AppInstallId)) + if err != nil { + return err + } + appModel, err := appRepo.GetFirst(commonRepo.WithByID(appInstall.AppId)) + if err != nil { + return err + } + + 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", req.Value); err != nil { + return err + } + } + global.LOG.Info("execute password change sql successful") + pass, err := encrypt.StringEncrypt(req.Value) + if err != nil { + return fmt.Errorf("decrypt database db password failed, err: %v", err) + } + _ = mysqlRepo.Update(mysqlData.ID, map[string]interface{}{"password": pass}) + return nil + } + + if err := updateInstallInfoInDB(req.Type, req.Database, "password", req.Value); err != nil { + return err + } + if req.From == "local" { + remote, err := databaseRepo.Get(commonRepo.WithByName(req.Database)) + if err != nil { + return err + } + pass, err := encrypt.StringEncrypt(req.Value) + if err != nil { + return fmt.Errorf("decrypt database password failed, err: %v", err) + } + _ = databaseRepo.Update(remote.ID, map[string]interface{}{"password": pass}) + } + return nil +} + +func (u *MysqlService) ChangeAccess(req dto.ChangeDBInfo) error { + if cmd.CheckIllegal(req.Value) { + return buserr.New(constant.ErrCmdIllegal) + } + cli, version, err := LoadMysqlClientByFrom(req.Database) + if err != nil { + return err + } + defer cli.Close() + var ( + mysqlData model.DatabaseMysql + accessInfo client.AccessChangeInfo + ) + accessInfo.Permission = req.Value + accessInfo.Timeout = 300 + accessInfo.Version = version + + if req.ID != 0 { + mysqlData, err = mysqlRepo.Get(commonRepo.WithByID(req.ID)) + if err != nil { + return err + } + accessInfo.Name = mysqlData.Name + accessInfo.Username = mysqlData.Username + accessInfo.Password = mysqlData.Password + accessInfo.OldPermission = mysqlData.Permission + } else { + accessInfo.Username = "root" + } + if err := cli.ChangeAccess(accessInfo); err != nil { + return err + } + + if mysqlData.ID != 0 { + _ = mysqlRepo.Update(mysqlData.ID, map[string]interface{}{"permission": req.Value}) + } + + return nil +} + +func (u *MysqlService) UpdateVariables(req dto.MysqlVariablesUpdate) error { + app, err := appInstallRepo.LoadBaseInfo(req.Type, req.Database) + if err != nil { + return err + } + var files []string + + path := fmt.Sprintf("%s/%s/%s/conf/my.cnf", constant.AppInstallDir, req.Type, app.Name) + lineBytes, err := os.ReadFile(path) + if err != nil { + return err + } + files = strings.Split(string(lineBytes), "\n") + + group := "[mysqld]" + for _, info := range req.Variables { + if !strings.HasPrefix(app.Version, "5.7") && !strings.HasPrefix(app.Version, "5.6") { + if info.Param == "query_cache_size" { + continue + } + } + + if _, ok := info.Value.(float64); ok { + files = updateMyCnf(files, group, info.Param, common.LoadSizeUnit(info.Value.(float64))) + } else { + files = updateMyCnf(files, group, info.Param, info.Value) + } + } + file, err := os.OpenFile(path, os.O_WRONLY|os.O_TRUNC, 0666) + if err != nil { + return err + } + defer file.Close() + _, err = file.WriteString(strings.Join(files, "\n")) + if err != nil { + return err + } + + if _, err := compose.Restart(fmt.Sprintf("%s/%s/%s/docker-compose.yml", constant.AppInstallDir, req.Type, app.Name)); err != nil { + return err + } + + return nil +} + +func (u *MysqlService) LoadRemoteAccess(req dto.OperationWithNameAndType) (bool, error) { + app, err := appInstallRepo.LoadBaseInfo(req.Type, req.Name) + if err != nil { + return false, err + } + hosts, err := executeSqlForRows(app.ContainerName, app.Key, app.Password, "select host from mysql.user where user='root';") + if err != nil { + return false, err + } + for _, host := range hosts { + if host == "%" { + return true, nil + } + } + + return false, nil +} + +func (u *MysqlService) LoadVariables(req dto.OperationWithNameAndType) (*dto.MysqlVariables, error) { + app, err := appInstallRepo.LoadBaseInfo(req.Type, req.Name) + if err != nil { + return nil, err + } + variableMap, err := executeSqlForMaps(app.ContainerName, app.Key, app.Password, "show global variables;") + if err != nil { + return nil, err + } + var info dto.MysqlVariables + arr, err := json.Marshal(variableMap) + if err != nil { + return nil, err + } + _ = json.Unmarshal(arr, &info) + return &info, nil +} + +func (u *MysqlService) LoadStatus(req dto.OperationWithNameAndType) (*dto.MysqlStatus, error) { + app, err := appInstallRepo.LoadBaseInfo(req.Type, req.Name) + if err != nil { + return nil, err + } + + statusMap, err := executeSqlForMaps(app.ContainerName, app.Key, app.Password, "show global status;") + if err != nil { + return nil, err + } + + var info dto.MysqlStatus + arr, err := json.Marshal(statusMap) + if err != nil { + return nil, err + } + _ = json.Unmarshal(arr, &info) + + if value, ok := statusMap["Run"]; ok { + uptime, _ := strconv.Atoi(value) + info.Run = time.Unix(time.Now().Unix()-int64(uptime), 0).Format(constant.DateTimeLayout) + } else { + if value, ok := statusMap["Uptime"]; ok { + uptime, _ := strconv.Atoi(value) + info.Run = time.Unix(time.Now().Unix()-int64(uptime), 0).Format(constant.DateTimeLayout) + } + } + + info.File = "OFF" + info.Position = "OFF" + rows, err := executeSqlForRows(app.ContainerName, app.Key, app.Password, "show master status;") + if err != nil { + return nil, err + } + if len(rows) > 2 { + itemValue := strings.Split(rows[1], "\t") + if len(itemValue) > 2 { + info.File = itemValue[0] + info.Position = itemValue[1] + } + } + + return &info, nil +} + +func executeSqlForMaps(containerName, dbType, password, command string) (map[string]string, error) { + cmd := exec.Command("docker", "exec", containerName, dbType, "-uroot", "-p"+password, "-e", command) + 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 nil, errors.New(stdStr) + } + + rows := strings.Split(stdStr, "\n") + rowMap := make(map[string]string) + for _, v := range rows { + itemRow := strings.Split(v, "\t") + if len(itemRow) == 2 { + rowMap[itemRow[0]] = itemRow[1] + } + } + return rowMap, nil +} + +func executeSqlForRows(containerName, dbType, password, command string) ([]string, error) { + cmd := exec.Command("docker", "exec", containerName, dbType, "-uroot", "-p"+password, "-e", command) + 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 nil, errors.New(stdStr) + } + return strings.Split(stdStr, "\n"), nil +} + +func updateMyCnf(oldFiles []string, group string, param string, value interface{}) []string { + isOn := false + hasGroup := false + hasKey := false + regItem, _ := regexp.Compile(`\[*\]`) + var newFiles []string + i := 0 + for _, line := range oldFiles { + i++ + if strings.HasPrefix(line, group) { + isOn = true + hasGroup = true + newFiles = append(newFiles, line) + continue + } + if !isOn { + newFiles = append(newFiles, line) + continue + } + if strings.HasPrefix(line, param+"=") || strings.HasPrefix(line, "# "+param+"=") { + newFiles = append(newFiles, fmt.Sprintf("%s=%v", param, value)) + hasKey = true + continue + } + if regItem.Match([]byte(line)) || i == len(oldFiles) { + isOn = false + if !hasKey { + newFiles = append(newFiles, fmt.Sprintf("%s=%v", param, value)) + } + newFiles = append(newFiles, line) + continue + } + newFiles = append(newFiles, line) + } + if !hasGroup { + newFiles = append(newFiles, group+"\n") + newFiles = append(newFiles, fmt.Sprintf("%s=%v\n", param, value)) + } + return newFiles +} + +func LoadMysqlClientByFrom(database string) (mysql.MysqlClient, string, error) { + var ( + dbInfo client.DBInfo + version string + err error + ) + + dbInfo.Timeout = 300 + databaseItem, err := databaseRepo.Get(commonRepo.WithByName(database)) + if err != nil { + return nil, "", err + } + dbInfo.Type = databaseItem.Type + dbInfo.From = databaseItem.From + dbInfo.Database = database + if dbInfo.From != "local" { + dbInfo.Address = databaseItem.Address + dbInfo.Port = databaseItem.Port + dbInfo.Username = databaseItem.Username + dbInfo.Password = databaseItem.Password + dbInfo.SSL = databaseItem.SSL + dbInfo.ClientKey = databaseItem.ClientKey + dbInfo.ClientCert = databaseItem.ClientCert + dbInfo.RootCert = databaseItem.RootCert + dbInfo.SkipVerify = databaseItem.SkipVerify + version = databaseItem.Version + + } else { + app, err := appInstallRepo.LoadBaseInfo(databaseItem.Type, database) + if err != nil { + return nil, "", err + } + dbInfo.Address = app.ContainerName + dbInfo.Username = "root" + dbInfo.Password = app.Password + version = app.Version + } + + cli, err := mysql.NewMysqlClient(dbInfo) + if err != nil { + return nil, "", err + } + return cli, version, nil +} diff --git a/agent/app/service/database_postgresql.go b/agent/app/service/database_postgresql.go new file mode 100644 index 000000000..740456d53 --- /dev/null +++ b/agent/app/service/database_postgresql.go @@ -0,0 +1,432 @@ +package service + +import ( + "context" + "fmt" + "os" + "path" + "strings" + + "github.com/1Panel-dev/1Panel/agent/app/dto" + "github.com/1Panel-dev/1Panel/agent/app/model" + "github.com/1Panel-dev/1Panel/agent/buserr" + "github.com/1Panel-dev/1Panel/agent/constant" + "github.com/1Panel-dev/1Panel/agent/global" + "github.com/1Panel-dev/1Panel/agent/utils/cmd" + "github.com/1Panel-dev/1Panel/agent/utils/encrypt" + "github.com/1Panel-dev/1Panel/agent/utils/postgresql" + "github.com/1Panel-dev/1Panel/agent/utils/postgresql/client" + _ "github.com/jackc/pgx/v5/stdlib" + "github.com/jinzhu/copier" + "github.com/pkg/errors" +) + +type PostgresqlService struct{} + +type IPostgresqlService interface { + SearchWithPage(search dto.PostgresqlDBSearch) (int64, interface{}, error) + ListDBOption() ([]dto.PostgresqlOption, error) + BindUser(req dto.PostgresqlBindUser) error + Create(ctx context.Context, req dto.PostgresqlDBCreate) (*model.DatabasePostgresql, error) + LoadFromRemote(database string) error + ChangePrivileges(req dto.PostgresqlPrivileges) error + ChangePassword(info dto.ChangeDBInfo) error + UpdateDescription(req dto.UpdateDescription) error + DeleteCheck(req dto.PostgresqlDBDeleteCheck) ([]string, error) + Delete(ctx context.Context, req dto.PostgresqlDBDelete) error +} + +func NewIPostgresqlService() IPostgresqlService { + return &PostgresqlService{} +} + +func (u *PostgresqlService) SearchWithPage(search dto.PostgresqlDBSearch) (int64, interface{}, error) { + total, postgresqls, err := postgresqlRepo.Page(search.Page, search.PageSize, + postgresqlRepo.WithByPostgresqlName(search.Database), + commonRepo.WithLikeName(search.Info), + commonRepo.WithOrderRuleBy(search.OrderBy, search.Order), + ) + var dtoPostgresqls []dto.PostgresqlDBInfo + for _, pg := range postgresqls { + var item dto.PostgresqlDBInfo + if err := copier.Copy(&item, &pg); err != nil { + return 0, nil, errors.WithMessage(constant.ErrStructTransform, err.Error()) + } + dtoPostgresqls = append(dtoPostgresqls, item) + } + return total, dtoPostgresqls, err +} + +func (u *PostgresqlService) ListDBOption() ([]dto.PostgresqlOption, error) { + postgresqls, err := postgresqlRepo.List() + if err != nil { + return nil, err + } + + databases, err := databaseRepo.GetList(databaseRepo.WithTypeList("postgresql,mariadb")) + if err != nil { + return nil, err + } + var dbs []dto.PostgresqlOption + for _, pg := range postgresqls { + var item dto.PostgresqlOption + if err := copier.Copy(&item, &pg); err != nil { + return nil, errors.WithMessage(constant.ErrStructTransform, err.Error()) + } + item.Database = pg.PostgresqlName + for _, database := range databases { + if database.Name == item.Database { + item.Type = database.Type + } + } + dbs = append(dbs, item) + } + return dbs, err +} + +func (u *PostgresqlService) BindUser(req dto.PostgresqlBindUser) error { + if cmd.CheckIllegal(req.Name, req.Username, req.Password) { + return buserr.New(constant.ErrCmdIllegal) + } + dbItem, err := postgresqlRepo.Get(postgresqlRepo.WithByPostgresqlName(req.Database), commonRepo.WithByName(req.Name)) + if err != nil { + return err + } + + pgClient, err := LoadPostgresqlClientByFrom(req.Database) + if err != nil { + return err + } + if err := pgClient.CreateUser(client.CreateInfo{ + Name: req.Name, + Username: req.Username, + Password: req.Password, + SuperUser: req.SuperUser, + Timeout: 300, + }, false); err != nil { + return err + } + pass, err := encrypt.StringEncrypt(req.Password) + if err != nil { + return fmt.Errorf("decrypt database db password failed, err: %v", err) + } + if err := postgresqlRepo.Update(dbItem.ID, map[string]interface{}{ + "username": req.Username, + "password": pass, + "super_user": req.SuperUser, + }); err != nil { + return err + } + return nil +} + +func (u *PostgresqlService) Create(ctx context.Context, req dto.PostgresqlDBCreate) (*model.DatabasePostgresql, error) { + if cmd.CheckIllegal(req.Name, req.Username, req.Password, req.Format) { + return nil, buserr.New(constant.ErrCmdIllegal) + } + + pgsql, _ := postgresqlRepo.Get(commonRepo.WithByName(req.Name), postgresqlRepo.WithByPostgresqlName(req.Database), databaseRepo.WithByFrom(req.From)) + if pgsql.ID != 0 { + return nil, constant.ErrRecordExist + } + + var createItem model.DatabasePostgresql + if err := copier.Copy(&createItem, &req); err != nil { + return nil, errors.WithMessage(constant.ErrStructTransform, err.Error()) + } + + if req.From == "local" && req.Username == "root" { + return nil, errors.New("Cannot set root as user name") + } + + cli, err := LoadPostgresqlClientByFrom(req.Database) + if err != nil { + return nil, err + } + defer cli.Close() + + createItem.PostgresqlName = req.Database + defer cli.Close() + if err := cli.Create(client.CreateInfo{ + Name: req.Name, + Username: req.Username, + Password: req.Password, + SuperUser: req.SuperUser, + Timeout: 300, + }); err != nil { + return nil, err + } + + global.LOG.Infof("create database %s successful!", req.Name) + if err := postgresqlRepo.Create(ctx, &createItem); err != nil { + return nil, err + } + return &createItem, nil +} + +func LoadPostgresqlClientByFrom(database string) (postgresql.PostgresqlClient, error) { + var ( + dbInfo client.DBInfo + err error + ) + + dbInfo.Timeout = 300 + databaseItem, err := databaseRepo.Get(commonRepo.WithByName(database)) + if err != nil { + return nil, err + } + dbInfo.From = databaseItem.From + dbInfo.Database = database + if dbInfo.From != "local" { + dbInfo.Address = databaseItem.Address + dbInfo.Port = databaseItem.Port + dbInfo.Username = databaseItem.Username + dbInfo.Password = databaseItem.Password + } else { + app, err := appInstallRepo.LoadBaseInfo(databaseItem.Type, database) + if err != nil { + return nil, err + } + dbInfo.From = "local" + dbInfo.Address = app.ContainerName + dbInfo.Username = app.UserName + dbInfo.Password = app.Password + dbInfo.Port = uint(app.Port) + } + + cli, err := postgresql.NewPostgresqlClient(dbInfo) + if err != nil { + return nil, err + } + return cli, nil +} + +func (u *PostgresqlService) LoadFromRemote(database string) error { + client, err := LoadPostgresqlClientByFrom(database) + if err != nil { + return err + } + defer client.Close() + + databases, err := postgresqlRepo.List(postgresqlRepo.WithByPostgresqlName(database)) + if err != nil { + return err + } + datas, err := client.SyncDB() + if err != nil { + return err + } + deleteList := databases + for _, data := range datas { + hasOld := false + for i := 0; i < len(databases); i++ { + if strings.EqualFold(databases[i].Name, data.Name) && strings.EqualFold(databases[i].PostgresqlName, data.PostgresqlName) { + hasOld = true + if databases[i].IsDelete { + _ = postgresqlRepo.Update(databases[i].ID, map[string]interface{}{"is_delete": false}) + } + deleteList = append(deleteList[:i], deleteList[i+1:]...) + break + } + } + if !hasOld { + var createItem model.DatabasePostgresql + if err := copier.Copy(&createItem, &data); err != nil { + return errors.WithMessage(constant.ErrStructTransform, err.Error()) + } + if err := postgresqlRepo.Create(context.Background(), &createItem); err != nil { + return err + } + } + } + for _, delItem := range deleteList { + _ = postgresqlRepo.Update(delItem.ID, map[string]interface{}{"is_delete": true}) + } + return nil +} + +func (u *PostgresqlService) UpdateDescription(req dto.UpdateDescription) error { + return postgresqlRepo.Update(req.ID, map[string]interface{}{"description": req.Description}) +} + +func (u *PostgresqlService) DeleteCheck(req dto.PostgresqlDBDeleteCheck) ([]string, error) { + var appInUsed []string + db, err := postgresqlRepo.Get(commonRepo.WithByID(req.ID)) + if err != nil { + return appInUsed, err + } + + if db.From == "local" { + app, err := appInstallRepo.LoadBaseInfo(req.Type, req.Database) + if err != nil { + return appInUsed, err + } + apps, _ := appInstallResourceRepo.GetBy(appInstallResourceRepo.WithLinkId(app.ID), appInstallResourceRepo.WithResourceId(db.ID)) + for _, app := range apps { + appInstall, _ := appInstallRepo.GetFirst(commonRepo.WithByID(app.AppInstallId)) + if appInstall.ID != 0 { + appInUsed = append(appInUsed, appInstall.Name) + } + } + } else { + apps, _ := appInstallResourceRepo.GetBy(appInstallResourceRepo.WithResourceId(db.ID), appRepo.WithKey(req.Type)) + for _, app := range apps { + appInstall, _ := appInstallRepo.GetFirst(commonRepo.WithByID(app.AppInstallId)) + if appInstall.ID != 0 { + appInUsed = append(appInUsed, appInstall.Name) + } + } + } + + return appInUsed, nil +} + +func (u *PostgresqlService) Delete(ctx context.Context, req dto.PostgresqlDBDelete) error { + db, err := postgresqlRepo.Get(commonRepo.WithByID(req.ID)) + if err != nil && !req.ForceDelete { + return err + } + cli, err := LoadPostgresqlClientByFrom(req.Database) + if err != nil { + return err + } + defer cli.Close() + if err := cli.Delete(client.DeleteInfo{ + Name: db.Name, + Username: db.Username, + ForceDelete: req.ForceDelete, + Timeout: 300, + }); err != nil && !req.ForceDelete { + return err + } + + if req.DeleteBackup { + uploadDir := path.Join(global.CONF.System.BaseDir, fmt.Sprintf("1panel/uploads/database/%s/%s/%s", req.Type, req.Database, db.Name)) + if _, err := os.Stat(uploadDir); err == nil { + _ = os.RemoveAll(uploadDir) + } + localDir, err := loadLocalDir() + if err != nil && !req.ForceDelete { + return err + } + backupDir := path.Join(localDir, fmt.Sprintf("database/%s/%s/%s", req.Type, db.PostgresqlName, db.Name)) + if _, err := os.Stat(backupDir); err == nil { + _ = os.RemoveAll(backupDir) + } + _ = backupRepo.DeleteRecord(ctx, commonRepo.WithByType(req.Type), commonRepo.WithByName(req.Database), backupRepo.WithByDetailName(db.Name)) + global.LOG.Infof("delete database %s-%s backups successful", req.Database, db.Name) + } + + _ = postgresqlRepo.Delete(ctx, commonRepo.WithByID(db.ID)) + return nil +} + +func (u *PostgresqlService) ChangePrivileges(req dto.PostgresqlPrivileges) error { + if cmd.CheckIllegal(req.Database, req.Username) { + return buserr.New(constant.ErrCmdIllegal) + } + dbItem, err := postgresqlRepo.Get(postgresqlRepo.WithByPostgresqlName(req.Database), commonRepo.WithByName(req.Name)) + if err != nil { + return err + } + cli, err := LoadPostgresqlClientByFrom(req.Database) + if err != nil { + return err + } + defer cli.Close() + + if err := cli.ChangePrivileges(client.Privileges{Username: req.Username, SuperUser: req.SuperUser, Timeout: 300}); err != nil { + return err + } + if err := postgresqlRepo.Update(dbItem.ID, map[string]interface{}{ + "super_user": req.SuperUser, + }); err != nil { + return err + } + return nil +} + +func (u *PostgresqlService) ChangePassword(req dto.ChangeDBInfo) error { + if cmd.CheckIllegal(req.Value) { + return buserr.New(constant.ErrCmdIllegal) + } + cli, err := LoadPostgresqlClientByFrom(req.Database) + if err != nil { + return err + } + defer cli.Close() + var ( + postgresqlData model.DatabasePostgresql + passwordInfo client.PasswordChangeInfo + ) + passwordInfo.Password = req.Value + passwordInfo.Timeout = 300 + + if req.ID != 0 { + postgresqlData, err = postgresqlRepo.Get(commonRepo.WithByID(req.ID)) + if err != nil { + return err + } + passwordInfo.Username = postgresqlData.Username + } else { + dbItem, err := databaseRepo.Get(commonRepo.WithByType(req.Type), commonRepo.WithByFrom(req.From)) + if err != nil { + return err + } + passwordInfo.Username = dbItem.Username + } + if err := cli.ChangePassword(passwordInfo); err != nil { + return err + } + + if req.ID != 0 { + var appRess []model.AppInstallResource + if req.From == "local" { + app, err := appInstallRepo.LoadBaseInfo(req.Type, req.Database) + if err != nil { + return err + } + appRess, _ = appInstallResourceRepo.GetBy(appInstallResourceRepo.WithLinkId(app.ID), appInstallResourceRepo.WithResourceId(postgresqlData.ID)) + } else { + appRess, _ = appInstallResourceRepo.GetBy(appInstallResourceRepo.WithResourceId(postgresqlData.ID)) + } + for _, appRes := range appRess { + appInstall, err := appInstallRepo.GetFirst(commonRepo.WithByID(appRes.AppInstallId)) + if err != nil { + return err + } + appModel, err := appRepo.GetFirst(commonRepo.WithByID(appInstall.AppId)) + if err != nil { + return err + } + + 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", req.Value); err != nil { + return err + } + } + global.LOG.Info("execute password change sql successful") + pass, err := encrypt.StringEncrypt(req.Value) + if err != nil { + return fmt.Errorf("decrypt database db password failed, err: %v", err) + } + _ = postgresqlRepo.Update(postgresqlData.ID, map[string]interface{}{"password": pass}) + return nil + } + + if err := updateInstallInfoInDB(req.Type, req.Database, "password", req.Value); err != nil { + return err + } + if req.From == "local" { + remote, err := databaseRepo.Get(commonRepo.WithByName(req.Database)) + if err != nil { + return err + } + pass, err := encrypt.StringEncrypt(req.Value) + if err != nil { + return fmt.Errorf("decrypt database password failed, err: %v", err) + } + _ = databaseRepo.Update(remote.ID, map[string]interface{}{"password": pass}) + } + return nil +} diff --git a/agent/app/service/database_redis.go b/agent/app/service/database_redis.go new file mode 100644 index 000000000..bc38bf5ad --- /dev/null +++ b/agent/app/service/database_redis.go @@ -0,0 +1,294 @@ +package service + +import ( + "context" + "encoding/json" + "errors" + "fmt" + "os" + "os/exec" + "strings" + + "github.com/1Panel-dev/1Panel/agent/app/dto" + "github.com/1Panel-dev/1Panel/agent/constant" + "github.com/1Panel-dev/1Panel/agent/utils/compose" + "github.com/1Panel-dev/1Panel/agent/utils/docker" + "github.com/1Panel-dev/1Panel/agent/utils/encrypt" + "github.com/docker/docker/api/types/container" + _ "github.com/go-sql-driver/mysql" +) + +type RedisService struct{} + +type IRedisService interface { + UpdateConf(req dto.RedisConfUpdate) error + UpdatePersistenceConf(req dto.RedisConfPersistenceUpdate) error + ChangePassword(info dto.ChangeRedisPass) error + + LoadStatus(req dto.OperationWithName) (*dto.RedisStatus, error) + LoadConf(req dto.OperationWithName) (*dto.RedisConf, error) + LoadPersistenceConf(req dto.OperationWithName) (*dto.RedisPersistence, error) + + CheckHasCli() bool + InstallCli() error +} + +func NewIRedisService() IRedisService { + return &RedisService{} +} + +func (u *RedisService) UpdateConf(req dto.RedisConfUpdate) error { + redisInfo, err := appInstallRepo.LoadBaseInfo("redis", req.Database) + if err != nil { + return err + } + + var confs []redisConfig + confs = append(confs, redisConfig{key: "timeout", value: req.Timeout}) + confs = append(confs, redisConfig{key: "maxclients", value: req.Maxclients}) + confs = append(confs, redisConfig{key: "maxmemory", value: req.Maxmemory}) + if err := confSet(redisInfo.Name, "", confs); err != nil { + return err + } + if _, err := compose.Restart(fmt.Sprintf("%s/redis/%s/docker-compose.yml", constant.AppInstallDir, redisInfo.Name)); err != nil { + return err + } + + return nil +} + +func (u *RedisService) CheckHasCli() bool { + client, err := docker.NewDockerClient() + if err != nil { + return false + } + defer client.Close() + containerLists, err := client.ContainerList(context.Background(), container.ListOptions{}) + if err != nil { + return false + } + for _, item := range containerLists { + if strings.ReplaceAll(item.Names[0], "/", "") == "1Panel-redis-cli-tools" { + return true + } + } + return false +} + +func (u *RedisService) InstallCli() error { + item := dto.ContainerOperate{ + Name: "1Panel-redis-cli-tools", + Image: "redis:7.2.4", + Network: "1panel-network", + } + return NewIContainerService().ContainerCreate(item) +} + +func (u *RedisService) ChangePassword(req dto.ChangeRedisPass) error { + if err := updateInstallInfoInDB("redis", req.Database, "password", req.Value); err != nil { + return err + } + remote, err := databaseRepo.Get(commonRepo.WithByName(req.Database)) + if err != nil { + return err + } + if remote.From == "local" { + pass, err := encrypt.StringEncrypt(req.Value) + if err != nil { + return fmt.Errorf("decrypt database password failed, err: %v", err) + } + _ = databaseRepo.Update(remote.ID, map[string]interface{}{"password": pass}) + } + + return nil +} + +func (u *RedisService) UpdatePersistenceConf(req dto.RedisConfPersistenceUpdate) error { + redisInfo, err := appInstallRepo.LoadBaseInfo("redis", req.Database) + if err != nil { + return err + } + + var confs []redisConfig + if req.Type == "rbd" { + confs = append(confs, redisConfig{key: "save", value: req.Save}) + } else { + confs = append(confs, redisConfig{key: "appendonly", value: req.Appendonly}) + confs = append(confs, redisConfig{key: "appendfsync", value: req.Appendfsync}) + } + if err := confSet(redisInfo.Name, req.Type, confs); err != nil { + return err + } + if _, err := compose.Restart(fmt.Sprintf("%s/redis/%s/docker-compose.yml", constant.AppInstallDir, redisInfo.Name)); err != nil { + return err + } + + return nil +} + +func (u *RedisService) LoadStatus(req dto.OperationWithName) (*dto.RedisStatus, error) { + redisInfo, err := appInstallRepo.LoadBaseInfo("redis", req.Name) + if err != nil { + return nil, err + } + commands := append(redisExec(redisInfo.ContainerName, redisInfo.Password), "info") + cmd := exec.Command("docker", commands...) + stdout, err := cmd.CombinedOutput() + if err != nil { + return nil, errors.New(string(stdout)) + } + rows := strings.Split(string(stdout), "\r\n") + rowMap := make(map[string]string) + for _, v := range rows { + itemRow := strings.Split(v, ":") + if len(itemRow) == 2 { + rowMap[itemRow[0]] = itemRow[1] + } + } + var info dto.RedisStatus + arr, err := json.Marshal(rowMap) + if err != nil { + return nil, err + } + _ = json.Unmarshal(arr, &info) + return &info, nil +} + +func (u *RedisService) LoadConf(req dto.OperationWithName) (*dto.RedisConf, error) { + redisInfo, err := appInstallRepo.LoadBaseInfo("redis", req.Name) + if err != nil { + return nil, err + } + + var item dto.RedisConf + item.ContainerName = redisInfo.ContainerName + item.Name = redisInfo.Name + item.Port = redisInfo.Port + item.Requirepass = redisInfo.Password + item.Timeout, _ = configGetStr(redisInfo.ContainerName, redisInfo.Password, "timeout") + item.Maxclients, _ = configGetStr(redisInfo.ContainerName, redisInfo.Password, "maxclients") + item.Maxmemory, _ = configGetStr(redisInfo.ContainerName, redisInfo.Password, "maxmemory") + return &item, nil +} + +func (u *RedisService) LoadPersistenceConf(req dto.OperationWithName) (*dto.RedisPersistence, error) { + redisInfo, err := appInstallRepo.LoadBaseInfo("redis", req.Name) + if err != nil { + return nil, err + } + var item dto.RedisPersistence + if item.Appendonly, err = configGetStr(redisInfo.ContainerName, redisInfo.Password, "appendonly"); err != nil { + return nil, err + } + if item.Appendfsync, err = configGetStr(redisInfo.ContainerName, redisInfo.Password, "appendfsync"); err != nil { + return nil, err + } + if item.Save, err = configGetStr(redisInfo.ContainerName, redisInfo.Password, "save"); err != nil { + return nil, err + } + return &item, nil +} + +func configGetStr(containerName, password, param string) (string, error) { + commands := append(redisExec(containerName, password), []string{"config", "get", param}...) + cmd := exec.Command("docker", commands...) + stdout, err := cmd.CombinedOutput() + if err != nil { + return "", errors.New(string(stdout)) + } + rows := strings.Split(string(stdout), "\r\n") + for _, v := range rows { + itemRow := strings.Split(v, "\n") + if len(itemRow) == 3 { + return itemRow[1], nil + } + } + return "", nil +} + +type redisConfig struct { + key string + value string +} + +func confSet(redisName string, updateType string, changeConf []redisConfig) error { + path := fmt.Sprintf("%s/redis/%s/conf/redis.conf", constant.AppInstallDir, redisName) + lineBytes, err := os.ReadFile(path) + if err != nil { + return err + } + files := strings.Split(string(lineBytes), "\n") + + startIndex, endIndex, emptyLine := 0, 0, 0 + var newFiles []string + for i := 0; i < len(files); i++ { + if files[i] == "# Redis configuration rewrite by 1Panel" { + startIndex = i + newFiles = append(newFiles, files[i]) + continue + } + if files[i] == "# End Redis configuration rewrite by 1Panel" { + endIndex = i + break + } + if startIndex == 0 && strings.HasPrefix(files[i], "save ") { + newFiles = append(newFiles, "# "+files[i]) + continue + } + if startIndex != 0 && endIndex == 0 && (len(files[i]) == 0 || (updateType == "rbd" && strings.HasPrefix(files[i], "save "))) { + emptyLine++ + continue + } + newFiles = append(newFiles, files[i]) + } + endIndex = endIndex - emptyLine + for _, item := range changeConf { + if item.key == "save" { + saveVal := strings.Split(item.value, ",") + for i := 0; i < len(saveVal); i++ { + newFiles = append(newFiles, "save "+saveVal[i]) + } + continue + } + + isExist := false + for i := startIndex; i < endIndex; i++ { + if strings.HasPrefix(newFiles[i], item.key) || strings.HasPrefix(newFiles[i], "# "+item.key) { + if item.value == "0" || len(item.value) == 0 { + newFiles[i] = fmt.Sprintf("# %s %s", item.key, item.value) + } else { + newFiles[i] = fmt.Sprintf("%s %s", item.key, item.value) + } + isExist = true + break + } + } + if !isExist { + if item.value == "0" || len(item.value) == 0 { + newFiles = append(newFiles, fmt.Sprintf("# %s %s", item.key, item.value)) + } else { + newFiles = append(newFiles, fmt.Sprintf("%s %s", item.key, item.value)) + } + } + } + newFiles = append(newFiles, "# End Redis configuration rewrite by 1Panel") + + file, err := os.OpenFile(path, os.O_WRONLY|os.O_TRUNC, 0666) + if err != nil { + return err + } + defer file.Close() + _, err = file.WriteString(strings.Join(newFiles, "\n")) + if err != nil { + return err + } + return nil +} + +func redisExec(containerName, password string) []string { + cmds := []string{"exec", containerName, "redis-cli", "-a", password, "--no-auth-warning"} + if len(password) == 0 { + cmds = []string{"exec", containerName, "redis-cli"} + } + return cmds +} diff --git a/agent/app/service/device.go b/agent/app/service/device.go new file mode 100644 index 000000000..eca21dd7a --- /dev/null +++ b/agent/app/service/device.go @@ -0,0 +1,431 @@ +package service + +import ( + "bufio" + "errors" + "fmt" + "net" + "os" + "path" + "strconv" + "strings" + "time" + + "github.com/1Panel-dev/1Panel/agent/app/dto" + "github.com/1Panel-dev/1Panel/agent/buserr" + "github.com/1Panel-dev/1Panel/agent/constant" + "github.com/1Panel-dev/1Panel/agent/global" + "github.com/1Panel-dev/1Panel/agent/utils/cmd" + "github.com/1Panel-dev/1Panel/agent/utils/common" + "github.com/1Panel-dev/1Panel/agent/utils/ntp" + "github.com/shirou/gopsutil/v3/mem" +) + +const defaultDNSPath = "/etc/resolv.conf" +const defaultHostPath = "/etc/hosts" +const defaultFstab = "/etc/fstab" + +type DeviceService struct{} + +type IDeviceService interface { + LoadBaseInfo() (dto.DeviceBaseInfo, error) + Update(key, value string) error + UpdateHosts(req []dto.HostHelper) error + UpdatePasswd(req dto.ChangePasswd) error + UpdateSwap(req dto.SwapHelper) error + UpdateByConf(req dto.UpdateByNameAndFile) error + LoadTimeZone() ([]string, error) + CheckDNS(key, value string) (bool, error) + LoadConf(name string) (string, error) + + Scan() dto.CleanData + Clean(req []dto.Clean) + CleanForCronjob() (string, error) +} + +func NewIDeviceService() IDeviceService { + return &DeviceService{} +} + +func (u *DeviceService) LoadBaseInfo() (dto.DeviceBaseInfo, error) { + var baseInfo dto.DeviceBaseInfo + baseInfo.LocalTime = time.Now().Format("2006-01-02 15:04:05 MST -0700") + baseInfo.TimeZone = common.LoadTimeZoneByCmd() + baseInfo.DNS = loadDNS() + baseInfo.Hosts = loadHosts() + baseInfo.Hostname = loadHostname() + baseInfo.User = loadUser() + ntp, err := settingRepo.Get(settingRepo.WithByKey("NtpSite")) + if err == nil { + baseInfo.Ntp = ntp.Value + } + + swapInfo, err := mem.SwapMemory() + if err != nil { + return baseInfo, err + } + baseInfo.SwapMemoryTotal = swapInfo.Total + baseInfo.SwapMemoryAvailable = swapInfo.Free + baseInfo.SwapMemoryUsed = swapInfo.Used + if baseInfo.SwapMemoryTotal != 0 { + baseInfo.SwapDetails = loadSwap() + } + disks := loadDiskInfo() + for _, item := range disks { + baseInfo.MaxSize += item.Free + } + + return baseInfo, nil +} + +func (u *DeviceService) LoadTimeZone() ([]string, error) { + std, err := cmd.Exec("timedatectl list-timezones") + if err != nil { + return []string{}, err + } + return strings.Split(std, "\n"), nil +} + +func (u *DeviceService) CheckDNS(key, value string) (bool, error) { + content, err := os.ReadFile(defaultDNSPath) + if err != nil { + return false, err + } + defer func() { _ = u.UpdateByConf(dto.UpdateByNameAndFile{Name: "DNS", File: string(content)}) }() + if key == "form" { + if err := u.Update("DNS", value); err != nil { + return false, err + } + } else { + if err := u.UpdateByConf(dto.UpdateByNameAndFile{Name: "DNS", File: value}); err != nil { + return false, err + } + } + + conn, err := net.DialTimeout("ip4:icmp", "www.baidu.com", time.Second*2) + if err != nil { + return false, err + } + defer conn.Close() + + return true, nil +} + +func (u *DeviceService) Update(key, value string) error { + switch key { + case "TimeZone": + if cmd.CheckIllegal(value) { + return buserr.New(constant.ErrCmdIllegal) + } + if err := ntp.UpdateSystemTimeZone(value); err != nil { + return err + } + go func() { + _, err := cmd.Exec("systemctl restart 1panel.service") + if err != nil { + global.LOG.Errorf("restart system for new time zone failed, err: %v", err) + } + }() + case "DNS": + if err := updateDNS(strings.Split(value, ",")); err != nil { + return err + } + case "Hostname": + if cmd.CheckIllegal(value) { + return buserr.New(constant.ErrCmdIllegal) + } + std, err := cmd.Execf("%s hostnamectl set-hostname %s", cmd.SudoHandleCmd(), value) + if err != nil { + return errors.New(std) + } + case "Ntp", "LocalTime": + if cmd.CheckIllegal(value) { + return buserr.New(constant.ErrCmdIllegal) + } + ntpValue := value + if key == "LocalTime" { + ntpItem, err := settingRepo.Get(settingRepo.WithByKey("NtpSite")) + if err != nil { + return err + } + ntpValue = ntpItem.Value + } else { + if err := settingRepo.Update("NtpSite", ntpValue); err != nil { + return err + } + } + ntime, err := ntp.GetRemoteTime(ntpValue) + if err != nil { + return err + } + ts := ntime.Format(constant.DateTimeLayout) + if err := ntp.UpdateSystemTime(ts); err != nil { + return err + } + default: + return fmt.Errorf("not support such key %s", key) + } + return nil +} + +func (u *DeviceService) UpdateHosts(req []dto.HostHelper) error { + conf, err := os.ReadFile(defaultHostPath) + if err != nil { + return fmt.Errorf("read namesever conf of %s failed, err: %v", defaultHostPath, err) + } + lines := strings.Split(string(conf), "\n") + newFile := "" + for _, line := range lines { + if len(line) == 0 { + continue + } + parts := strings.Fields(line) + if len(parts) < 2 { + newFile += line + "\n" + continue + } + for index, item := range req { + if item.IP == parts[0] && item.Host == strings.Join(parts[1:], " ") { + newFile += line + "\n" + req = append(req[:index], req[index+1:]...) + break + } + } + } + for _, item := range req { + newFile += fmt.Sprintf("%s %s \n", item.IP, item.Host) + } + file, err := os.OpenFile(defaultHostPath, os.O_WRONLY|os.O_TRUNC, 0640) + if err != nil { + return err + } + defer file.Close() + write := bufio.NewWriter(file) + _, _ = write.WriteString(newFile) + write.Flush() + return nil +} + +func (u *DeviceService) UpdatePasswd(req dto.ChangePasswd) error { + if cmd.CheckIllegal(req.User, req.Passwd) { + return buserr.New(constant.ErrCmdIllegal) + } + std, err := cmd.Execf("%s echo '%s:%s' | %s chpasswd", cmd.SudoHandleCmd(), req.User, req.Passwd, cmd.SudoHandleCmd()) + if err != nil { + if strings.Contains(err.Error(), "does not exist") { + return buserr.New(constant.ErrNotExistUser) + } + return errors.New(std) + } + return nil +} + +func (u *DeviceService) UpdateSwap(req dto.SwapHelper) error { + if cmd.CheckIllegal(req.Path) { + return buserr.New(constant.ErrCmdIllegal) + } + if !req.IsNew { + std, err := cmd.Execf("%s swapoff %s", cmd.SudoHandleCmd(), req.Path) + if err != nil { + return fmt.Errorf("handle swapoff %s failed, err: %s", req.Path, std) + } + } + if req.Size == 0 { + if req.Path == path.Join(global.CONF.System.BaseDir, ".1panel_swap") { + _ = os.Remove(path.Join(global.CONF.System.BaseDir, ".1panel_swap")) + } + return operateSwapWithFile(true, req) + } + std1, err := cmd.Execf("%s dd if=/dev/zero of=%s bs=1024 count=%d", cmd.SudoHandleCmd(), req.Path, req.Size) + if err != nil { + return fmt.Errorf("handle dd path %s failed, err: %s", req.Path, std1) + } + std2, err := cmd.Execf("%s mkswap -f %s", cmd.SudoHandleCmd(), req.Path) + if err != nil { + return fmt.Errorf("handle dd path %s failed, err: %s", req.Path, std2) + } + std3, err := cmd.Execf("%s swapon %s", cmd.SudoHandleCmd(), req.Path) + if err != nil { + _, _ = cmd.Execf("%s swapoff %s", cmd.SudoHandleCmd(), req.Path) + return fmt.Errorf("handle dd path %s failed, err: %s", req.Path, std3) + } + return operateSwapWithFile(false, req) +} + +func (u *DeviceService) LoadConf(name string) (string, error) { + pathItem := "" + switch name { + case "DNS": + pathItem = defaultDNSPath + case "Hosts": + pathItem = defaultHostPath + default: + return "", fmt.Errorf("not support such name %s", name) + } + if _, err := os.Stat(pathItem); err != nil { + return "", err + } + content, err := os.ReadFile(pathItem) + if err != nil { + return "", err + } + return string(content), nil +} + +func (u *DeviceService) UpdateByConf(req dto.UpdateByNameAndFile) error { + if req.Name != "DNS" && req.Name != "Hosts" { + return fmt.Errorf("not support such name %s", req.Name) + } + path := defaultDNSPath + if req.Name == "Hosts" { + path = defaultHostPath + } + file, err := os.OpenFile(path, os.O_WRONLY|os.O_TRUNC, 0640) + if err != nil { + return err + } + defer file.Close() + write := bufio.NewWriter(file) + _, _ = write.WriteString(req.File) + write.Flush() + return nil +} + +func loadDNS() []string { + var list []string + dnsConf, err := os.ReadFile(defaultDNSPath) + if err == nil { + lines := strings.Split(string(dnsConf), "\n") + for _, line := range lines { + if strings.HasPrefix(line, "nameserver ") { + list = append(list, strings.TrimPrefix(line, "nameserver ")) + } + } + } + return list +} + +func updateDNS(list []string) error { + conf, err := os.ReadFile(defaultDNSPath) + if err != nil { + return fmt.Errorf("read nameserver conf of %s failed, err: %v", defaultDNSPath, err) + } + lines := strings.Split(string(conf), "\n") + newFile := "" + for _, line := range lines { + if len(line) == 0 { + continue + } + parts := strings.Fields(line) + if len(parts) < 2 || parts[0] != "nameserver" { + newFile += line + "\n" + continue + } + itemNs := strings.Join(parts[1:], " ") + for index, item := range list { + if item == itemNs { + newFile += line + "\n" + list = append(list[:index], list[index+1:]...) + break + } + } + } + for _, item := range list { + if len(item) != 0 { + newFile += fmt.Sprintf("nameserver %s \n", item) + } + } + file, err := os.OpenFile(defaultDNSPath, os.O_WRONLY|os.O_TRUNC, 0640) + if err != nil { + return err + } + defer file.Close() + write := bufio.NewWriter(file) + _, _ = write.WriteString(newFile) + write.Flush() + return nil +} + +func loadHosts() []dto.HostHelper { + var list []dto.HostHelper + hostConf, err := os.ReadFile(defaultHostPath) + if err == nil { + lines := strings.Split(string(hostConf), "\n") + for _, line := range lines { + parts := strings.Fields(line) + if len(parts) < 2 || strings.HasPrefix(strings.TrimPrefix(line, " "), "#") { + continue + } + list = append(list, dto.HostHelper{IP: parts[0], Host: strings.Join(parts[1:], " ")}) + } + } + return list +} + +func loadHostname() string { + std, err := cmd.Exec("hostname") + if err != nil { + return "" + } + return strings.ReplaceAll(std, "\n", "") +} + +func loadUser() string { + std, err := cmd.Exec("whoami") + if err != nil { + return "" + } + return strings.ReplaceAll(std, "\n", "") +} + +func loadSwap() []dto.SwapHelper { + var data []dto.SwapHelper + std, err := cmd.Execf("%s swapon --summary", cmd.SudoHandleCmd()) + if err != nil { + return data + } + lines := strings.Split(std, "\n") + for index, line := range lines { + if index == 0 { + continue + } + parts := strings.Fields(line) + if len(parts) < 5 { + continue + } + sizeItem, _ := strconv.Atoi(parts[2]) + data = append(data, dto.SwapHelper{Path: parts[0], Size: uint64(sizeItem), Used: parts[3]}) + } + return data +} + +func operateSwapWithFile(delete bool, req dto.SwapHelper) error { + conf, err := os.ReadFile(defaultFstab) + if err != nil { + return fmt.Errorf("read file %s failed, err: %v", defaultFstab, err) + } + lines := strings.Split(string(conf), "\n") + newFile := "" + for _, line := range lines { + if len(line) == 0 { + continue + } + parts := strings.Fields(line) + if len(parts) == 6 && parts[0] == req.Path { + continue + } + newFile += line + "\n" + } + if !delete { + newFile += fmt.Sprintf("%s swap swap defaults 0 0\n", req.Path) + } + file, err := os.OpenFile(defaultFstab, os.O_WRONLY|os.O_TRUNC, 0640) + if err != nil { + return err + } + defer file.Close() + write := bufio.NewWriter(file) + _, _ = write.WriteString(newFile) + write.Flush() + return nil +} diff --git a/agent/app/service/device_clean.go b/agent/app/service/device_clean.go new file mode 100644 index 000000000..4b36b997c --- /dev/null +++ b/agent/app/service/device_clean.go @@ -0,0 +1,735 @@ +package service + +import ( + "context" + "fmt" + "os" + "path" + "sort" + "strings" + "time" + + "github.com/1Panel-dev/1Panel/agent/constant" + "github.com/1Panel-dev/1Panel/agent/utils/docker" + "github.com/docker/docker/api/types" + "github.com/docker/docker/api/types/filters" + + "github.com/1Panel-dev/1Panel/agent/app/dto" + "github.com/1Panel-dev/1Panel/agent/global" + "github.com/1Panel-dev/1Panel/agent/utils/cmd" + "github.com/1Panel-dev/1Panel/agent/utils/common" + fileUtils "github.com/1Panel-dev/1Panel/agent/utils/files" + "github.com/google/uuid" +) + +const ( + upgradePath = "1panel/tmp/upgrade" + snapshotTmpPath = "1panel/tmp/system" + rollbackPath = "1panel/tmp" + cachePath = "1panel/cache" + oldOriginalPath = "original" + oldAppBackupPath = "1panel/resource/apps_bak" + oldDownloadPath = "1panel/tmp/download" + oldUpgradePath = "1panel/tmp" + tmpUploadPath = "1panel/tmp/upload" + uploadPath = "1panel/uploads" + downloadPath = "1panel/download" + logPath = "1panel/log" + dockerLogPath = "1panel/tmp/docker_logs" + taskPath = "1panel/task" +) + +func (u *DeviceService) Scan() dto.CleanData { + var ( + SystemClean dto.CleanData + treeData []dto.CleanTree + ) + fileOp := fileUtils.NewFileOp() + + originalPath := path.Join(global.CONF.System.BaseDir, "1panel_original") + originalSize, _ := fileOp.GetDirSize(originalPath) + treeData = append(treeData, dto.CleanTree{ + ID: uuid.NewString(), + Label: "1panel_original", + Size: uint64(originalSize), + IsCheck: true, + IsRecommend: true, + Type: "1panel_original", + Children: loadTreeWithDir(true, "1panel_original", originalPath, fileOp), + }) + + upgradePath := path.Join(global.CONF.System.BaseDir, upgradePath) + upgradeSize, _ := fileOp.GetDirSize(upgradePath) + treeData = append(treeData, dto.CleanTree{ + ID: uuid.NewString(), + Label: "upgrade", + Size: uint64(upgradeSize), + IsCheck: false, + IsRecommend: true, + Type: "upgrade", + Children: loadTreeWithDir(true, "upgrade", upgradePath, fileOp), + }) + + snapTree := loadSnapshotTree(fileOp) + snapSize := uint64(0) + for _, snap := range snapTree { + snapSize += snap.Size + } + treeData = append(treeData, dto.CleanTree{ + ID: uuid.NewString(), + Label: "snapshot", + Size: snapSize, + IsCheck: true, + IsRecommend: true, + Type: "snapshot", + Children: snapTree, + }) + + rollBackTree := loadRollBackTree(fileOp) + rollbackSize := uint64(0) + for _, rollback := range rollBackTree { + rollbackSize += rollback.Size + } + treeData = append(treeData, dto.CleanTree{ + ID: uuid.NewString(), + Label: "rollback", + Size: rollbackSize, + IsCheck: true, + IsRecommend: true, + Type: "rollback", + Children: rollBackTree, + }) + + cachePath := path.Join(global.CONF.System.BaseDir, cachePath) + cacheSize, _ := fileOp.GetDirSize(cachePath) + treeData = append(treeData, dto.CleanTree{ + ID: uuid.NewString(), + Label: "cache", + Size: uint64(cacheSize), + IsCheck: false, + IsRecommend: false, + Type: "cache", + }) + + unusedTree := loadUnusedFile(fileOp) + unusedSize := uint64(0) + for _, unused := range unusedTree { + unusedSize += unused.Size + } + treeData = append(treeData, dto.CleanTree{ + ID: uuid.NewString(), + Label: "unused", + Size: unusedSize, + IsCheck: true, + IsRecommend: true, + Type: "unused", + Children: unusedTree, + }) + SystemClean.SystemClean = treeData + + uploadTreeData := loadUploadTree(fileOp) + SystemClean.UploadClean = append(SystemClean.UploadClean, uploadTreeData...) + + downloadTreeData := loadDownloadTree(fileOp) + SystemClean.DownloadClean = append(SystemClean.DownloadClean, downloadTreeData...) + + logTree := loadLogTree(fileOp) + SystemClean.SystemLogClean = append(SystemClean.SystemLogClean, logTree...) + + containerTree := loadContainerTree() + SystemClean.ContainerClean = append(SystemClean.ContainerClean, containerTree...) + + return SystemClean +} + +func (u *DeviceService) Clean(req []dto.Clean) { + size := uint64(0) + restart := false + for _, item := range req { + size += item.Size + switch item.TreeType { + case "1panel_original": + dropFileOrDir(path.Join(global.CONF.System.BaseDir, "1panel_original", item.Name)) + + case "upgrade": + dropFileOrDir(path.Join(global.CONF.System.BaseDir, upgradePath, item.Name)) + + case "snapshot": + dropFileOrDir(path.Join(global.CONF.System.BaseDir, snapshotTmpPath, item.Name)) + dropFileOrDir(path.Join(global.CONF.System.Backup, "system", item.Name)) + case "snapshot_tmp": + dropFileOrDir(path.Join(global.CONF.System.BaseDir, snapshotTmpPath, item.Name)) + case "snapshot_local": + dropFileOrDir(path.Join(global.CONF.System.Backup, "system", item.Name)) + + case "rollback": + dropFileOrDir(path.Join(global.CONF.System.BaseDir, rollbackPath, "app")) + dropFileOrDir(path.Join(global.CONF.System.BaseDir, rollbackPath, "database")) + dropFileOrDir(path.Join(global.CONF.System.BaseDir, rollbackPath, "website")) + case "rollback_app": + dropFileOrDir(path.Join(global.CONF.System.BaseDir, rollbackPath, "app", item.Name)) + case "rollback_database": + dropFileOrDir(path.Join(global.CONF.System.BaseDir, rollbackPath, "database", item.Name)) + case "rollback_website": + dropFileOrDir(path.Join(global.CONF.System.BaseDir, rollbackPath, "website", item.Name)) + + case "cache": + dropFileOrDir(path.Join(global.CONF.System.BaseDir, cachePath, item.Name)) + restart = true + + case "unused": + dropFileOrDir(path.Join(global.CONF.System.BaseDir, oldOriginalPath)) + dropFileOrDir(path.Join(global.CONF.System.BaseDir, oldAppBackupPath)) + dropFileOrDir(path.Join(global.CONF.System.BaseDir, oldDownloadPath)) + files, _ := os.ReadDir(path.Join(global.CONF.System.BaseDir, oldUpgradePath)) + if len(files) == 0 { + continue + } + for _, file := range files { + if strings.HasPrefix(file.Name(), "upgrade_") { + dropFileOrDir(path.Join(global.CONF.System.BaseDir, oldUpgradePath, file.Name())) + } + } + case "old_original": + dropFileOrDir(path.Join(global.CONF.System.BaseDir, oldOriginalPath, item.Name)) + case "old_apps_bak": + dropFileOrDir(path.Join(global.CONF.System.BaseDir, oldAppBackupPath, item.Name)) + case "old_download": + dropFileOrDir(path.Join(global.CONF.System.BaseDir, oldDownloadPath, item.Name)) + case "old_upgrade": + if len(item.Name) == 0 { + files, _ := os.ReadDir(path.Join(global.CONF.System.BaseDir, oldUpgradePath)) + if len(files) == 0 { + continue + } + for _, file := range files { + if strings.HasPrefix(file.Name(), "upgrade_") { + dropFileOrDir(path.Join(global.CONF.System.BaseDir, oldUpgradePath, file.Name())) + } + } + } else { + dropFileOrDir(path.Join(global.CONF.System.BaseDir, oldUpgradePath, item.Name)) + } + + case "upload": + dropFileOrDir(path.Join(global.CONF.System.BaseDir, uploadPath, item.Name)) + if len(item.Name) == 0 { + dropFileOrDir(path.Join(global.CONF.System.BaseDir, tmpUploadPath)) + } + case "upload_tmp": + dropFileOrDir(path.Join(global.CONF.System.BaseDir, tmpUploadPath, item.Name)) + case "upload_app": + dropFileOrDir(path.Join(global.CONF.System.BaseDir, uploadPath, "app", item.Name)) + case "upload_database": + dropFileOrDir(path.Join(global.CONF.System.BaseDir, uploadPath, "database", item.Name)) + case "upload_website": + dropFileOrDir(path.Join(global.CONF.System.BaseDir, uploadPath, "website", item.Name)) + case "upload_directory": + dropFileOrDir(path.Join(global.CONF.System.BaseDir, uploadPath, "directory", item.Name)) + case "download": + dropFileOrDir(path.Join(global.CONF.System.BaseDir, downloadPath, item.Name)) + case "download_app": + dropFileOrDir(path.Join(global.CONF.System.BaseDir, downloadPath, "app", item.Name)) + case "download_database": + dropFileOrDir(path.Join(global.CONF.System.BaseDir, downloadPath, "database", item.Name)) + case "download_website": + dropFileOrDir(path.Join(global.CONF.System.BaseDir, downloadPath, "website", item.Name)) + case "download_directory": + dropFileOrDir(path.Join(global.CONF.System.BaseDir, downloadPath, "directory", item.Name)) + + case "system_log": + if len(item.Name) == 0 { + files, _ := os.ReadDir(path.Join(global.CONF.System.BaseDir, logPath)) + if len(files) == 0 { + continue + } + for _, file := range files { + if file.Name() == "1Panel.log" || file.IsDir() { + continue + } + dropFileOrDir(path.Join(global.CONF.System.BaseDir, logPath, file.Name())) + } + } else { + dropFileOrDir(path.Join(global.CONF.System.BaseDir, logPath, item.Name)) + } + case "docker_log": + dropFileOrDir(path.Join(global.CONF.System.BaseDir, dockerLogPath, item.Name)) + case "task_log": + pathItem := path.Join(global.CONF.System.BaseDir, taskPath, item.Name) + dropFileOrDir(path.Join(global.CONF.System.BaseDir, taskPath, item.Name)) + if len(item.Name) == 0 { + files, _ := os.ReadDir(pathItem) + if len(files) == 0 { + continue + } + for _, file := range files { + _ = cronjobRepo.DeleteRecord(cronjobRepo.WithByRecordFile(path.Join(pathItem, file.Name()))) + } + } else { + _ = cronjobRepo.DeleteRecord(cronjobRepo.WithByRecordFile(pathItem)) + } + case "images": + dropImages() + case "containers": + dropContainers() + case "volumes": + dropVolumes() + case "build_cache": + dropBuildCache() + } + } + + _ = settingRepo.Update("LastCleanTime", time.Now().Format(constant.DateTimeLayout)) + _ = settingRepo.Update("LastCleanSize", fmt.Sprintf("%v", size)) + _ = settingRepo.Update("LastCleanData", fmt.Sprintf("%v", len(req))) + + if restart { + go func() { + _, err := cmd.Exec("systemctl restart 1panel.service") + if err != nil { + global.LOG.Errorf("restart system port failed, err: %v", err) + } + }() + } +} + +func (u *DeviceService) CleanForCronjob() (string, error) { + logs := "" + size := int64(0) + fileCount := 0 + dropFileOrDirWithLog(path.Join(global.CONF.System.BaseDir, "1panel_original"), &logs, &size, &fileCount) + + upgradePath := path.Join(global.CONF.System.BaseDir, upgradePath) + upgradeFiles, _ := os.ReadDir(upgradePath) + if len(upgradeFiles) != 0 { + sort.Slice(upgradeFiles, func(i, j int) bool { + return upgradeFiles[i].Name() > upgradeFiles[j].Name() + }) + for i := 0; i < len(upgradeFiles); i++ { + if i != 0 { + dropFileOrDirWithLog(path.Join(upgradePath, upgradeFiles[i].Name()), &logs, &size, &fileCount) + } + } + } + + dropFileOrDirWithLog(path.Join(global.CONF.System.BaseDir, snapshotTmpPath), &logs, &size, &fileCount) + dropFileOrDirWithLog(path.Join(global.CONF.System.Backup, "system"), &logs, &size, &fileCount) + + dropFileOrDirWithLog(path.Join(global.CONF.System.BaseDir, rollbackPath, "app"), &logs, &size, &fileCount) + dropFileOrDirWithLog(path.Join(global.CONF.System.BaseDir, rollbackPath, "website"), &logs, &size, &fileCount) + dropFileOrDirWithLog(path.Join(global.CONF.System.BaseDir, rollbackPath, "database"), &logs, &size, &fileCount) + + dropFileOrDirWithLog(path.Join(global.CONF.System.BaseDir, oldOriginalPath), &logs, &size, &fileCount) + dropFileOrDirWithLog(path.Join(global.CONF.System.BaseDir, oldAppBackupPath), &logs, &size, &fileCount) + dropFileOrDirWithLog(path.Join(global.CONF.System.BaseDir, oldDownloadPath), &logs, &size, &fileCount) + oldUpgradePath := path.Join(global.CONF.System.BaseDir, oldUpgradePath) + oldUpgradeFiles, _ := os.ReadDir(oldUpgradePath) + if len(oldUpgradeFiles) != 0 { + for i := 0; i < len(oldUpgradeFiles); i++ { + dropFileOrDirWithLog(path.Join(oldUpgradePath, oldUpgradeFiles[i].Name()), &logs, &size, &fileCount) + } + } + + dropFileOrDirWithLog(path.Join(global.CONF.System.BaseDir, tmpUploadPath), &logs, &size, &fileCount) + dropFileOrDirWithLog(path.Join(global.CONF.System.BaseDir, uploadPath), &logs, &size, &fileCount) + dropFileOrDirWithLog(path.Join(global.CONF.System.BaseDir, downloadPath), &logs, &size, &fileCount) + + logPath := path.Join(global.CONF.System.BaseDir, logPath) + logFiles, _ := os.ReadDir(logPath) + if len(logFiles) != 0 { + for i := 0; i < len(logFiles); i++ { + if logFiles[i].Name() != "1Panel.log" { + dropFileOrDirWithLog(path.Join(logPath, logFiles[i].Name()), &logs, &size, &fileCount) + } + } + } + timeNow := time.Now().Format(constant.DateTimeLayout) + dropFileOrDirWithLog(path.Join(global.CONF.System.BaseDir, dockerLogPath), &logs, &size, &fileCount) + logs += fmt.Sprintf("\n%s: total clean: %s, total count: %d", timeNow, common.LoadSizeUnit2F(float64(size)), fileCount) + + _ = settingRepo.Update("LastCleanTime", timeNow) + _ = settingRepo.Update("LastCleanSize", fmt.Sprintf("%v", size)) + _ = settingRepo.Update("LastCleanData", fmt.Sprintf("%v", fileCount)) + + return logs, nil +} + +func loadSnapshotTree(fileOp fileUtils.FileOp) []dto.CleanTree { + var treeData []dto.CleanTree + path1 := path.Join(global.CONF.System.BaseDir, snapshotTmpPath) + list1 := loadTreeWithAllFile(true, path1, "snapshot_tmp", path1, fileOp) + if len(list1) != 0 { + size, _ := fileOp.GetDirSize(path1) + treeData = append(treeData, dto.CleanTree{ID: uuid.NewString(), Label: "snapshot_tmp", Size: uint64(size), Children: list1, Type: "snapshot_tmp", IsRecommend: true}) + } + + path2 := path.Join(global.CONF.System.Backup, "system") + list2 := loadTreeWithAllFile(true, path2, "snapshot_local", path2, fileOp) + if len(list2) != 0 { + size, _ := fileOp.GetDirSize(path2) + treeData = append(treeData, dto.CleanTree{ID: uuid.NewString(), Label: "snapshot_local", Size: uint64(size), Children: list2, Type: "snapshot_local", IsRecommend: true}) + } + return treeData +} + +func loadRollBackTree(fileOp fileUtils.FileOp) []dto.CleanTree { + var treeData []dto.CleanTree + path1 := path.Join(global.CONF.System.BaseDir, rollbackPath, "app") + list1 := loadTreeWithAllFile(true, path1, "rollback_app", path1, fileOp) + size1, _ := fileOp.GetDirSize(path1) + treeData = append(treeData, dto.CleanTree{ID: uuid.NewString(), Label: "rollback_app", Size: uint64(size1), Children: list1, Type: "rollback_app", IsRecommend: true}) + + path2 := path.Join(global.CONF.System.BaseDir, rollbackPath, "website") + list2 := loadTreeWithAllFile(true, path2, "rollback_website", path2, fileOp) + size2, _ := fileOp.GetDirSize(path2) + treeData = append(treeData, dto.CleanTree{ID: uuid.NewString(), Label: "rollback_website", Size: uint64(size2), Children: list2, Type: "rollback_website", IsRecommend: true}) + + path3 := path.Join(global.CONF.System.BaseDir, rollbackPath, "database") + list3 := loadTreeWithAllFile(true, path3, "rollback_database", path3, fileOp) + size3, _ := fileOp.GetDirSize(path3) + treeData = append(treeData, dto.CleanTree{ID: uuid.NewString(), Label: "rollback_database", Size: uint64(size3), Children: list3, Type: "rollback_database", IsRecommend: true}) + + return treeData +} + +func loadUnusedFile(fileOp fileUtils.FileOp) []dto.CleanTree { + var treeData []dto.CleanTree + path1 := path.Join(global.CONF.System.BaseDir, oldOriginalPath) + list1 := loadTreeWithAllFile(true, path1, "old_original", path1, fileOp) + if len(list1) != 0 { + size, _ := fileOp.GetDirSize(path1) + treeData = append(treeData, dto.CleanTree{ID: uuid.NewString(), Label: "old_original", Size: uint64(size), Children: list1, Type: "old_original"}) + } + + path2 := path.Join(global.CONF.System.BaseDir, oldAppBackupPath) + list2 := loadTreeWithAllFile(true, path2, "old_apps_bak", path2, fileOp) + if len(list2) != 0 { + size, _ := fileOp.GetDirSize(path2) + treeData = append(treeData, dto.CleanTree{ID: uuid.NewString(), Label: "old_apps_bak", Size: uint64(size), Children: list2, Type: "old_apps_bak"}) + } + + path3 := path.Join(global.CONF.System.BaseDir, oldDownloadPath) + list3 := loadTreeWithAllFile(true, path3, "old_download", path3, fileOp) + if len(list3) != 0 { + size, _ := fileOp.GetDirSize(path3) + treeData = append(treeData, dto.CleanTree{ID: uuid.NewString(), Label: "old_download", Size: uint64(size), Children: list3, Type: "old_download"}) + } + + path4 := path.Join(global.CONF.System.BaseDir, oldUpgradePath) + list4 := loadTreeWithDir(true, "old_upgrade", path4, fileOp) + itemSize := uint64(0) + for _, item := range list4 { + itemSize += item.Size + } + if len(list4) != 0 { + treeData = append(treeData, dto.CleanTree{ID: uuid.NewString(), Label: "old_upgrade", Size: itemSize, Children: list4, Type: "old_upgrade"}) + } + return treeData +} + +func loadUploadTree(fileOp fileUtils.FileOp) []dto.CleanTree { + var treeData []dto.CleanTree + + path0 := path.Join(global.CONF.System.BaseDir, tmpUploadPath) + list0 := loadTreeWithAllFile(true, path0, "upload_tmp", path0, fileOp) + size0, _ := fileOp.GetDirSize(path0) + treeData = append(treeData, dto.CleanTree{ID: uuid.NewString(), Label: "upload_tmp", Size: uint64(size0), Children: list0, Type: "upload_tmp", IsRecommend: true}) + + path1 := path.Join(global.CONF.System.BaseDir, uploadPath, "app") + list1 := loadTreeWithAllFile(true, path1, "upload_app", path1, fileOp) + size1, _ := fileOp.GetDirSize(path1) + treeData = append(treeData, dto.CleanTree{ID: uuid.NewString(), Label: "upload_app", Size: uint64(size1), Children: list1, Type: "upload_app", IsRecommend: true}) + + path2 := path.Join(global.CONF.System.BaseDir, uploadPath, "website") + list2 := loadTreeWithAllFile(true, path2, "upload_website", path2, fileOp) + size2, _ := fileOp.GetDirSize(path2) + treeData = append(treeData, dto.CleanTree{ID: uuid.NewString(), Label: "upload_website", Size: uint64(size2), Children: list2, Type: "upload_website", IsRecommend: true}) + + path3 := path.Join(global.CONF.System.BaseDir, uploadPath, "database") + list3 := loadTreeWithAllFile(true, path3, "upload_database", path3, fileOp) + size3, _ := fileOp.GetDirSize(path3) + treeData = append(treeData, dto.CleanTree{ID: uuid.NewString(), Label: "upload_database", Size: uint64(size3), Children: list3, Type: "upload_database", IsRecommend: true}) + + path4 := path.Join(global.CONF.System.BaseDir, uploadPath, "directory") + list4 := loadTreeWithAllFile(true, path4, "upload_directory", path4, fileOp) + size4, _ := fileOp.GetDirSize(path4) + treeData = append(treeData, dto.CleanTree{ID: uuid.NewString(), Label: "upload_directory", Size: uint64(size4), Children: list4, Type: "upload_directory", IsRecommend: true}) + + path5 := path.Join(global.CONF.System.BaseDir, uploadPath) + uploadTreeData := loadTreeWithAllFile(true, path5, "upload", path5, fileOp) + treeData = append(treeData, uploadTreeData...) + + return treeData +} + +func loadDownloadTree(fileOp fileUtils.FileOp) []dto.CleanTree { + var treeData []dto.CleanTree + path1 := path.Join(global.CONF.System.BaseDir, downloadPath, "app") + list1 := loadTreeWithAllFile(true, path1, "download_app", path1, fileOp) + size1, _ := fileOp.GetDirSize(path1) + treeData = append(treeData, dto.CleanTree{ID: uuid.NewString(), Label: "download_app", Size: uint64(size1), Children: list1, Type: "download_app", IsRecommend: true}) + + path2 := path.Join(global.CONF.System.BaseDir, downloadPath, "website") + list2 := loadTreeWithAllFile(true, path2, "download_website", path2, fileOp) + size2, _ := fileOp.GetDirSize(path2) + treeData = append(treeData, dto.CleanTree{ID: uuid.NewString(), Label: "download_website", Size: uint64(size2), Children: list2, Type: "download_website", IsRecommend: true}) + + path3 := path.Join(global.CONF.System.BaseDir, downloadPath, "database") + list3 := loadTreeWithAllFile(true, path3, "download_database", path3, fileOp) + size3, _ := fileOp.GetDirSize(path3) + treeData = append(treeData, dto.CleanTree{ID: uuid.NewString(), Label: "download_database", Size: uint64(size3), Children: list3, Type: "download_database", IsRecommend: true}) + + path4 := path.Join(global.CONF.System.BaseDir, downloadPath, "directory") + list4 := loadTreeWithAllFile(true, path4, "download_directory", path4, fileOp) + size4, _ := fileOp.GetDirSize(path4) + treeData = append(treeData, dto.CleanTree{ID: uuid.NewString(), Label: "download_directory", Size: uint64(size4), Children: list4, Type: "download_directory", IsRecommend: true}) + + path5 := path.Join(global.CONF.System.BaseDir, downloadPath) + uploadTreeData := loadTreeWithAllFile(true, path5, "download", path5, fileOp) + treeData = append(treeData, uploadTreeData...) + + return treeData +} + +func loadLogTree(fileOp fileUtils.FileOp) []dto.CleanTree { + var treeData []dto.CleanTree + path1 := path.Join(global.CONF.System.BaseDir, logPath) + list1 := loadTreeWithAllFile(true, path1, "system_log", path1, fileOp) + size := uint64(0) + for _, file := range list1 { + size += file.Size + } + treeData = append(treeData, dto.CleanTree{ID: uuid.NewString(), Label: "system_log", Size: uint64(size), Children: list1, Type: "system_log", IsRecommend: true}) + + path2 := path.Join(global.CONF.System.BaseDir, dockerLogPath) + list2 := loadTreeWithAllFile(true, path2, "docker_log", path2, fileOp) + size2, _ := fileOp.GetDirSize(path2) + treeData = append(treeData, dto.CleanTree{ID: uuid.NewString(), Label: "docker_log", Size: uint64(size2), Children: list2, Type: "docker_log", IsRecommend: true}) + + path3 := path.Join(global.CONF.System.BaseDir, taskPath) + list3 := loadTreeWithAllFile(false, path3, "task_log", path3, fileOp) + size3, _ := fileOp.GetDirSize(path3) + treeData = append(treeData, dto.CleanTree{ID: uuid.NewString(), Label: "task_log", Size: uint64(size3), Children: list3, Type: "task_log"}) + return treeData +} + +func loadContainerTree() []dto.CleanTree { + var treeData []dto.CleanTree + client, err := docker.NewDockerClient() + if err != nil { + return treeData + } + diskUsage, err := client.DiskUsage(context.Background(), types.DiskUsageOptions{}) + if err != nil { + return treeData + } + imageSize := uint64(0) + for _, file := range diskUsage.Images { + if file.Containers == 0 { + imageSize += uint64(file.Size) + } + } + treeData = append(treeData, dto.CleanTree{ID: uuid.NewString(), Label: "container_images", Size: imageSize, Children: nil, Type: "images", IsRecommend: true}) + + containerSize := uint64(0) + for _, file := range diskUsage.Containers { + if file.State != "running" { + containerSize += uint64(file.SizeRw) + } + } + treeData = append(treeData, dto.CleanTree{ID: uuid.NewString(), Label: "container_containers", Size: containerSize, Children: nil, Type: "containers", IsRecommend: true}) + + volumeSize := uint64(0) + for _, file := range diskUsage.Volumes { + if file.UsageData.RefCount <= 0 { + volumeSize += uint64(file.UsageData.Size) + } + } + treeData = append(treeData, dto.CleanTree{ID: uuid.NewString(), Label: "container_volumes", Size: volumeSize, Children: nil, Type: "volumes", IsRecommend: true}) + + var buildCacheTotalSize int64 + for _, cache := range diskUsage.BuildCache { + if cache.Type == "source.local" { + buildCacheTotalSize += cache.Size + } + } + treeData = append(treeData, dto.CleanTree{ID: uuid.NewString(), Label: "build_cache", Size: uint64(buildCacheTotalSize), Type: "build_cache", IsRecommend: true}) + return treeData +} + +func loadTreeWithDir(isCheck bool, treeType, pathItem string, fileOp fileUtils.FileOp) []dto.CleanTree { + var lists []dto.CleanTree + files, err := os.ReadDir(pathItem) + if err != nil { + return lists + } + sort.Slice(files, func(i, j int) bool { + return files[i].Name() > files[j].Name() + }) + for _, file := range files { + if (treeType == "old_upgrade" || treeType == "upgrade") && !strings.HasPrefix(file.Name(), "upgrade_2023") { + continue + } + if file.IsDir() { + size, err := fileOp.GetDirSize(path.Join(pathItem, file.Name())) + if err != nil { + continue + } + item := dto.CleanTree{ + ID: uuid.NewString(), + Label: file.Name(), + Type: treeType, + Size: uint64(size), + Name: strings.TrimPrefix(file.Name(), "/"), + IsCheck: isCheck, + IsRecommend: isCheck, + } + if treeType == "upgrade" && len(lists) == 0 { + item.IsCheck = false + item.IsRecommend = false + } + lists = append(lists, item) + } + } + return lists +} + +func loadTreeWithAllFile(isCheck bool, originalPath, treeType, pathItem string, fileOp fileUtils.FileOp) []dto.CleanTree { + var lists []dto.CleanTree + + files, err := os.ReadDir(pathItem) + if err != nil { + return lists + } + for _, file := range files { + if treeType == "system_log" && (file.Name() == "1Panel.log" || file.IsDir()) { + continue + } + if (treeType == "upload" || treeType == "download") && file.IsDir() && (file.Name() == "app" || file.Name() == "database" || file.Name() == "website" || file.Name() == "directory") { + continue + } + size := uint64(0) + name := strings.TrimPrefix(path.Join(pathItem, file.Name()), originalPath+"/") + if file.IsDir() { + sizeItem, err := fileOp.GetDirSize(path.Join(pathItem, file.Name())) + if err != nil { + continue + } + size = uint64(sizeItem) + } else { + fileInfo, err := file.Info() + if err != nil { + continue + } + size = uint64(fileInfo.Size()) + } + item := dto.CleanTree{ + ID: uuid.NewString(), + Label: file.Name(), + Type: treeType, + Size: uint64(size), + Name: name, + IsCheck: isCheck, + IsRecommend: isCheck, + } + if file.IsDir() { + item.Children = loadTreeWithAllFile(isCheck, originalPath, treeType, path.Join(pathItem, file.Name()), fileOp) + } + lists = append(lists, item) + } + return lists +} + +func dropFileOrDir(itemPath string) { + global.LOG.Debugf("drop file %s", itemPath) + if err := os.RemoveAll(itemPath); err != nil { + global.LOG.Errorf("drop file %s failed, err %v", itemPath, err) + } +} + +func dropBuildCache() { + client, err := docker.NewDockerClient() + if err != nil { + global.LOG.Errorf("do not get docker client") + } + opts := types.BuildCachePruneOptions{} + opts.All = true + _, err = client.BuildCachePrune(context.Background(), opts) + if err != nil { + global.LOG.Errorf("drop build cache failed, err %v", err) + } +} + +func dropImages() { + client, err := docker.NewDockerClient() + if err != nil { + global.LOG.Errorf("do not get docker client") + } + pruneFilters := filters.NewArgs() + pruneFilters.Add("dangling", "false") + _, err = client.ImagesPrune(context.Background(), pruneFilters) + if err != nil { + global.LOG.Errorf("drop images failed, err %v", err) + } +} + +func dropContainers() { + client, err := docker.NewDockerClient() + if err != nil { + global.LOG.Errorf("do not get docker client") + } + pruneFilters := filters.NewArgs() + _, err = client.ContainersPrune(context.Background(), pruneFilters) + if err != nil { + global.LOG.Errorf("drop containers failed, err %v", err) + } +} + +func dropVolumes() { + client, err := docker.NewDockerClient() + if err != nil { + global.LOG.Errorf("do not get docker client") + } + pruneFilters := filters.NewArgs() + versions, err := client.ServerVersion(context.Background()) + if err != nil { + global.LOG.Errorf("do not get docker api versions") + } + if common.ComparePanelVersion(versions.APIVersion, "1.42") { + pruneFilters.Add("all", "true") + } + _, err = client.VolumesPrune(context.Background(), pruneFilters) + if err != nil { + global.LOG.Errorf("drop volumes failed, err %v", err) + } +} + +func dropFileOrDirWithLog(itemPath string, log *string, size *int64, count *int) { + itemSize := int64(0) + itemCount := 0 + scanFile(itemPath, &itemSize, &itemCount) + *size += itemSize + *count += itemCount + if err := os.RemoveAll(itemPath); err != nil { + global.LOG.Errorf("drop file %s failed, err %v", itemPath, err) + *log += fmt.Sprintf("- drop file %s failed, err: %v \n\n", itemPath, err) + return + } + *log += fmt.Sprintf("+ drop file %s successful!, size: %s, count: %d \n\n", itemPath, common.LoadSizeUnit2F(float64(itemSize)), itemCount) +} + +func scanFile(pathItem string, size *int64, count *int) { + files, _ := os.ReadDir(pathItem) + for _, f := range files { + if f.IsDir() { + scanFile(path.Join(pathItem, f.Name()), size, count) + } else { + fileInfo, err := f.Info() + if err != nil { + continue + } + *count++ + *size += fileInfo.Size() + } + } +} diff --git a/agent/app/service/docker.go b/agent/app/service/docker.go new file mode 100644 index 000000000..45d2bad30 --- /dev/null +++ b/agent/app/service/docker.go @@ -0,0 +1,388 @@ +package service + +import ( + "bufio" + "context" + "encoding/json" + "os" + "path" + "strings" + + "github.com/1Panel-dev/1Panel/agent/app/dto" + "github.com/1Panel-dev/1Panel/agent/constant" + "github.com/1Panel-dev/1Panel/agent/utils/cmd" + "github.com/1Panel-dev/1Panel/agent/utils/docker" + "github.com/pkg/errors" +) + +type DockerService struct{} + +type IDockerService interface { + UpdateConf(req dto.SettingUpdate) error + UpdateLogOption(req dto.LogOption) error + UpdateIpv6Option(req dto.Ipv6Option) error + UpdateConfByFile(info dto.DaemonJsonUpdateByFile) error + LoadDockerStatus() string + LoadDockerConf() *dto.DaemonJsonConf + OperateDocker(req dto.DockerOperation) error +} + +func NewIDockerService() IDockerService { + return &DockerService{} +} + +type daemonJsonItem struct { + Status string `json:"status"` + Mirrors []string `json:"registry-mirrors"` + Registries []string `json:"insecure-registries"` + LiveRestore bool `json:"live-restore"` + Ipv6 bool `json:"ipv6"` + FixedCidrV6 string `json:"fixed-cidr-v6"` + Ip6Tables bool `json:"ip6tables"` + Experimental bool `json:"experimental"` + IPTables bool `json:"iptables"` + ExecOpts []string `json:"exec-opts"` + LogOption logOption `json:"log-opts"` +} +type logOption struct { + LogMaxSize string `json:"max-size"` + LogMaxFile string `json:"max-file"` +} + +func (u *DockerService) LoadDockerStatus() string { + client, err := docker.NewDockerClient() + if err != nil { + return constant.Stopped + } + defer client.Close() + if _, err := client.Ping(context.Background()); err != nil { + return constant.Stopped + } + + return constant.StatusRunning +} + +func (u *DockerService) LoadDockerConf() *dto.DaemonJsonConf { + ctx := context.Background() + var data dto.DaemonJsonConf + data.IPTables = true + data.Status = constant.StatusRunning + data.Version = "-" + client, err := docker.NewDockerClient() + if err != nil { + data.Status = constant.Stopped + } else { + defer client.Close() + if _, err := client.Ping(ctx); err != nil { + data.Status = constant.Stopped + } + itemVersion, err := client.ServerVersion(ctx) + if err == nil { + data.Version = itemVersion.Version + } + } + data.IsSwarm = false + stdout2, _ := cmd.Exec("docker info | grep Swarm") + if string(stdout2) == " Swarm: active\n" { + data.IsSwarm = true + } + if _, err := os.Stat(constant.DaemonJsonPath); err != nil { + return &data + } + file, err := os.ReadFile(constant.DaemonJsonPath) + if err != nil { + return &data + } + var conf daemonJsonItem + daemonMap := make(map[string]interface{}) + if err := json.Unmarshal(file, &daemonMap); err != nil { + return &data + } + arr, err := json.Marshal(daemonMap) + if err != nil { + return &data + } + if err := json.Unmarshal(arr, &conf); err != nil { + return &data + } + if _, ok := daemonMap["iptables"]; !ok { + conf.IPTables = true + } + data.CgroupDriver = "cgroupfs" + for _, opt := range conf.ExecOpts { + if strings.HasPrefix(opt, "native.cgroupdriver=") { + data.CgroupDriver = strings.ReplaceAll(opt, "native.cgroupdriver=", "") + break + } + } + data.Ipv6 = conf.Ipv6 + data.FixedCidrV6 = conf.FixedCidrV6 + data.Ip6Tables = conf.Ip6Tables + data.Experimental = conf.Experimental + data.LogMaxSize = conf.LogOption.LogMaxSize + data.LogMaxFile = conf.LogOption.LogMaxFile + data.Mirrors = conf.Mirrors + data.Registries = conf.Registries + data.IPTables = conf.IPTables + data.LiveRestore = conf.LiveRestore + return &data +} + +func (u *DockerService) UpdateConf(req dto.SettingUpdate) error { + err := createIfNotExistDaemonJsonFile() + if err != nil { + return err + } + file, err := os.ReadFile(constant.DaemonJsonPath) + if err != nil { + return err + } + daemonMap := make(map[string]interface{}) + _ = json.Unmarshal(file, &daemonMap) + + switch req.Key { + case "Registries": + req.Value = strings.TrimSuffix(req.Value, ",") + if len(req.Value) == 0 { + delete(daemonMap, "insecure-registries") + } else { + daemonMap["insecure-registries"] = strings.Split(req.Value, ",") + } + case "Mirrors": + req.Value = strings.TrimSuffix(req.Value, ",") + if len(req.Value) == 0 { + delete(daemonMap, "registry-mirrors") + } else { + daemonMap["registry-mirrors"] = strings.Split(req.Value, ",") + } + case "Ipv6": + if req.Value == "disable" { + delete(daemonMap, "ipv6") + delete(daemonMap, "fixed-cidr-v6") + delete(daemonMap, "ip6tables") + delete(daemonMap, "experimental") + } + case "LogOption": + if req.Value == "disable" { + delete(daemonMap, "log-opts") + } + case "LiveRestore": + if req.Value == "disable" { + delete(daemonMap, "live-restore") + } else { + daemonMap["live-restore"] = true + } + case "IPtables": + if req.Value == "enable" { + delete(daemonMap, "iptables") + } else { + daemonMap["iptables"] = false + } + case "Driver": + if opts, ok := daemonMap["exec-opts"]; ok { + if optsValue, isArray := opts.([]interface{}); isArray { + for i := 0; i < len(optsValue); i++ { + if opt, isStr := optsValue[i].(string); isStr { + if strings.HasPrefix(opt, "native.cgroupdriver=") { + optsValue[i] = "native.cgroupdriver=" + req.Value + break + } + } + } + } + } else { + if req.Value == "systemd" { + daemonMap["exec-opts"] = []string{"native.cgroupdriver=systemd"} + } + } + } + if len(daemonMap) == 0 { + _ = os.Remove(constant.DaemonJsonPath) + return nil + } + 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(string(stdout)) + } + return nil +} +func createIfNotExistDaemonJsonFile() error { + if _, err := os.Stat(constant.DaemonJsonPath); err != nil && os.IsNotExist(err) { + if err = os.MkdirAll(path.Dir(constant.DaemonJsonPath), os.ModePerm); err != nil { + return err + } + var daemonFile *os.File + daemonFile, err = os.Create(constant.DaemonJsonPath) + if err != nil { + return err + } + defer daemonFile.Close() + } + return nil +} + +func (u *DockerService) UpdateLogOption(req dto.LogOption) error { + err := createIfNotExistDaemonJsonFile() + if err != nil { + return err + } + file, err := os.ReadFile(constant.DaemonJsonPath) + if err != nil { + return err + } + daemonMap := make(map[string]interface{}) + _ = json.Unmarshal(file, &daemonMap) + + changeLogOption(daemonMap, req.LogMaxFile, req.LogMaxSize) + if len(daemonMap) == 0 { + _ = os.Remove(constant.DaemonJsonPath) + return nil + } + 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(string(stdout)) + } + return nil +} + +func (u *DockerService) UpdateIpv6Option(req dto.Ipv6Option) error { + err := createIfNotExistDaemonJsonFile() + if err != nil { + return err + } + + file, err := os.ReadFile(constant.DaemonJsonPath) + if err != nil { + return err + } + daemonMap := make(map[string]interface{}) + _ = json.Unmarshal(file, &daemonMap) + + daemonMap["ipv6"] = true + daemonMap["fixed-cidr-v6"] = req.FixedCidrV6 + if req.Ip6Tables { + daemonMap["ip6tables"] = req.Ip6Tables + } + if req.Experimental { + daemonMap["experimental"] = req.Experimental + } + if len(daemonMap) == 0 { + _ = os.Remove(constant.DaemonJsonPath) + return nil + } + 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(string(stdout)) + } + return nil +} + +func (u *DockerService) UpdateConfByFile(req dto.DaemonJsonUpdateByFile) error { + if len(req.File) == 0 { + _ = os.Remove(constant.DaemonJsonPath) + return nil + } + err := createIfNotExistDaemonJsonFile() + if err != nil { + return err + } + file, err := os.OpenFile(constant.DaemonJsonPath, os.O_WRONLY|os.O_TRUNC, 0640) + if err != nil { + return err + } + defer file.Close() + write := bufio.NewWriter(file) + _, _ = write.WriteString(req.File) + write.Flush() + + stdout, err := cmd.Exec("systemctl restart docker") + if err != nil { + return errors.New(string(stdout)) + } + return nil +} + +func (u *DockerService) OperateDocker(req dto.DockerOperation) error { + service := "docker" + if req.Operation == "stop" { + service = "docker.socket" + } + stdout, err := cmd.Execf("systemctl %s %s ", req.Operation, service) + if err != nil { + return errors.New(string(stdout)) + } + return nil +} + +func changeLogOption(daemonMap map[string]interface{}, logMaxFile, logMaxSize string) { + if opts, ok := daemonMap["log-opts"]; ok { + if len(logMaxFile) != 0 || len(logMaxSize) != 0 { + daemonMap["log-driver"] = "json-file" + } + optsMap, isMap := opts.(map[string]interface{}) + if isMap { + if len(logMaxFile) != 0 { + optsMap["max-file"] = logMaxFile + } else { + delete(optsMap, "max-file") + } + if len(logMaxSize) != 0 { + optsMap["max-size"] = logMaxSize + } else { + delete(optsMap, "max-size") + } + if len(optsMap) == 0 { + delete(daemonMap, "log-opts") + } + } else { + optsMap := make(map[string]interface{}) + if len(logMaxFile) != 0 { + optsMap["max-file"] = logMaxFile + } + if len(logMaxSize) != 0 { + optsMap["max-size"] = logMaxSize + } + if len(optsMap) != 0 { + daemonMap["log-opts"] = optsMap + } + } + } else { + if len(logMaxFile) != 0 || len(logMaxSize) != 0 { + daemonMap["log-driver"] = "json-file" + } + optsMap := make(map[string]interface{}) + if len(logMaxFile) != 0 { + optsMap["max-file"] = logMaxFile + } + if len(logMaxSize) != 0 { + optsMap["max-size"] = logMaxSize + } + if len(optsMap) != 0 { + daemonMap["log-opts"] = optsMap + } + } +} diff --git a/agent/app/service/entry.go b/agent/app/service/entry.go new file mode 100644 index 000000000..25b3289a5 --- /dev/null +++ b/agent/app/service/entry.go @@ -0,0 +1,46 @@ +package service + +import "github.com/1Panel-dev/1Panel/agent/app/repo" + +var ( + commonRepo = repo.NewCommonRepo() + + appRepo = repo.NewIAppRepo() + appTagRepo = repo.NewIAppTagRepo() + appDetailRepo = repo.NewIAppDetailRepo() + tagRepo = repo.NewITagRepo() + appInstallRepo = repo.NewIAppInstallRepo() + appInstallResourceRepo = repo.NewIAppInstallResourceRpo() + + mysqlRepo = repo.NewIMysqlRepo() + postgresqlRepo = repo.NewIPostgresqlRepo() + databaseRepo = repo.NewIDatabaseRepo() + + imageRepoRepo = repo.NewIImageRepoRepo() + composeRepo = repo.NewIComposeTemplateRepo() + + cronjobRepo = repo.NewICronjobRepo() + + hostRepo = repo.NewIHostRepo() + groupRepo = repo.NewIGroupRepo() + commandRepo = repo.NewICommandRepo() + ftpRepo = repo.NewIFtpRepo() + clamRepo = repo.NewIClamRepo() + + settingRepo = repo.NewISettingRepo() + backupRepo = repo.NewIBackupRepo() + + websiteRepo = repo.NewIWebsiteRepo() + websiteDomainRepo = repo.NewIWebsiteDomainRepo() + websiteDnsRepo = repo.NewIWebsiteDnsAccountRepo() + websiteSSLRepo = repo.NewISSLRepo() + websiteAcmeRepo = repo.NewIAcmeAccountRepo() + websiteCARepo = repo.NewIWebsiteCARepo() + + snapshotRepo = repo.NewISnapshotRepo() + + runtimeRepo = repo.NewIRunTimeRepo() + phpExtensionsRepo = repo.NewIPHPExtensionsRepo() + + favoriteRepo = repo.NewIFavoriteRepo() +) diff --git a/agent/app/service/fail2ban.go b/agent/app/service/fail2ban.go new file mode 100644 index 000000000..b0f2af483 --- /dev/null +++ b/agent/app/service/fail2ban.go @@ -0,0 +1,251 @@ +package service + +import ( + "bufio" + "fmt" + "os" + "strconv" + "strings" + + "github.com/1Panel-dev/1Panel/agent/app/dto" + "github.com/1Panel-dev/1Panel/agent/buserr" + "github.com/1Panel-dev/1Panel/agent/utils/firewall" + "github.com/1Panel-dev/1Panel/agent/utils/toolbox" +) + +const defaultFail2BanPath = "/etc/fail2ban/jail.local" + +type Fail2BanService struct{} + +type IFail2BanService interface { + LoadBaseInfo() (dto.Fail2BanBaseInfo, error) + Search(search dto.Fail2BanSearch) ([]string, error) + Operate(operation string) error + OperateSSHD(req dto.Fail2BanSet) error + UpdateConf(req dto.Fail2BanUpdate) error + UpdateConfByFile(req dto.UpdateByFile) error +} + +func NewIFail2BanService() IFail2BanService { + return &Fail2BanService{} +} + +func (u *Fail2BanService) LoadBaseInfo() (dto.Fail2BanBaseInfo, error) { + var baseInfo dto.Fail2BanBaseInfo + client, err := toolbox.NewFail2Ban() + if err != nil { + return baseInfo, err + } + baseInfo.IsEnable, baseInfo.IsActive, baseInfo.IsExist = client.Status() + if !baseInfo.IsActive { + baseInfo.Version = "-" + } else { + baseInfo.Version = client.Version() + } + conf, err := os.ReadFile(defaultFail2BanPath) + if err != nil { + if baseInfo.IsActive { + return baseInfo, fmt.Errorf("read fail2ban conf of %s failed, err: %v", defaultFail2BanPath, err) + } else { + return baseInfo, nil + } + } + lines := strings.Split(string(conf), "\n") + + block := "" + for _, line := range lines { + if strings.HasPrefix(strings.ToLower(line), "[default]") { + block = "default" + continue + } + if strings.HasPrefix(line, "[sshd]") { + block = "sshd" + continue + } + if strings.HasPrefix(line, "[") { + block = "" + continue + } + if block != "default" && block != "sshd" { + continue + } + loadFailValue(line, &baseInfo) + } + + return baseInfo, nil +} + +func (u *Fail2BanService) Search(req dto.Fail2BanSearch) ([]string, error) { + var list []string + client, err := toolbox.NewFail2Ban() + if err != nil { + return nil, err + } + if req.Status == "banned" { + list, err = client.ListBanned() + + } else { + list, err = client.ListIgnore() + } + if err != nil { + return nil, err + } + + return list, nil +} + +func (u *Fail2BanService) Operate(operation string) error { + client, err := toolbox.NewFail2Ban() + if err != nil { + return err + } + return client.Operate(operation) +} + +func (u *Fail2BanService) UpdateConf(req dto.Fail2BanUpdate) error { + if req.Key == "banaction" { + if req.Value == "firewallcmd-ipset" || req.Value == "ufw" { + itemName := "ufw" + if req.Value == "firewallcmd-ipset" { + itemName = "firewalld" + } + client, err := firewall.NewFirewallClient() + if err != nil { + return err + } + if client.Name() != itemName { + return buserr.WithName("ErrBanAction", itemName) + } + status, _ := client.Status() + if status != "running" { + return buserr.WithName("ErrBanAction", itemName) + } + } + } + if req.Key == "logpath" { + if _, err := os.Stat(req.Value); err != nil { + return err + } + } + conf, err := os.ReadFile(defaultFail2BanPath) + if err != nil { + return fmt.Errorf("read fail2ban conf of %s failed, err: %v", defaultFail2BanPath, err) + } + lines := strings.Split(string(conf), "\n") + + isStart, isEnd, hasKey := false, false, false + newFile := "" + for index, line := range lines { + if !isStart && strings.HasPrefix(line, "[sshd]") { + isStart = true + newFile += fmt.Sprintf("%s\n", line) + continue + } + if !isStart || isEnd { + newFile += fmt.Sprintf("%s\n", line) + continue + } + if strings.HasPrefix(line, req.Key) { + hasKey = true + newFile += fmt.Sprintf("%s = %s\n", req.Key, req.Value) + continue + } + if strings.HasPrefix(line, "[") || index == len(lines)-1 { + isEnd = true + if !hasKey { + newFile += fmt.Sprintf("%s = %s\n", req.Key, req.Value) + } + } + newFile += line + if index != len(lines)-1 { + newFile += "\n" + } + } + file, err := os.OpenFile(defaultFail2BanPath, os.O_WRONLY|os.O_TRUNC, 0640) + if err != nil { + return err + } + defer file.Close() + write := bufio.NewWriter(file) + _, _ = write.WriteString(newFile) + write.Flush() + + client, err := toolbox.NewFail2Ban() + if err != nil { + return err + } + if err := client.Operate("restart"); err != nil { + return err + } + return nil +} + +func (u *Fail2BanService) UpdateConfByFile(req dto.UpdateByFile) error { + file, err := os.OpenFile(defaultFail2BanPath, os.O_WRONLY|os.O_TRUNC, 0640) + if err != nil { + return err + } + defer file.Close() + write := bufio.NewWriter(file) + _, _ = write.WriteString(req.File) + write.Flush() + + client, err := toolbox.NewFail2Ban() + if err != nil { + return err + } + if err := client.Operate("restart"); err != nil { + return err + } + return nil +} + +func (u *Fail2BanService) OperateSSHD(req dto.Fail2BanSet) error { + if req.Operate == "ignore" { + if err := u.UpdateConf(dto.Fail2BanUpdate{Key: "ignoreip", Value: strings.Join(req.IPs, ",")}); err != nil { + return err + } + return nil + } + client, err := toolbox.NewFail2Ban() + if err != nil { + return err + } + if err := client.ReBanIPs(req.IPs); err != nil { + return err + } + return nil +} + +func loadFailValue(line string, baseInfo *dto.Fail2BanBaseInfo) { + if strings.HasPrefix(line, "port") { + itemValue := strings.ReplaceAll(line, "port", "") + itemValue = strings.ReplaceAll(itemValue, "=", "") + baseInfo.Port, _ = strconv.Atoi(strings.TrimSpace(itemValue)) + } + if strings.HasPrefix(line, "maxretry") { + itemValue := strings.ReplaceAll(line, "maxretry", "") + itemValue = strings.ReplaceAll(itemValue, "=", "") + baseInfo.MaxRetry, _ = strconv.Atoi(strings.TrimSpace(itemValue)) + } + if strings.HasPrefix(line, "findtime") { + itemValue := strings.ReplaceAll(line, "findtime", "") + itemValue = strings.ReplaceAll(itemValue, "=", "") + baseInfo.FindTime = strings.TrimSpace(itemValue) + } + if strings.HasPrefix(line, "bantime") { + itemValue := strings.ReplaceAll(line, "bantime", "") + itemValue = strings.ReplaceAll(itemValue, "=", "") + baseInfo.BanTime = strings.TrimSpace(itemValue) + } + if strings.HasPrefix(line, "banaction") { + itemValue := strings.ReplaceAll(line, "banaction", "") + itemValue = strings.ReplaceAll(itemValue, "=", "") + baseInfo.BanAction = strings.TrimSpace(itemValue) + } + if strings.HasPrefix(line, "logpath") { + itemValue := strings.ReplaceAll(line, "logpath", "") + itemValue = strings.ReplaceAll(itemValue, "=", "") + baseInfo.LogPath = strings.TrimSpace(itemValue) + } +} diff --git a/agent/app/service/favorite.go b/agent/app/service/favorite.go new file mode 100644 index 000000000..2a44ee31b --- /dev/null +++ b/agent/app/service/favorite.go @@ -0,0 +1,83 @@ +package service + +import ( + "github.com/1Panel-dev/1Panel/agent/app/dto" + "github.com/1Panel-dev/1Panel/agent/app/dto/request" + "github.com/1Panel-dev/1Panel/agent/app/dto/response" + "github.com/1Panel-dev/1Panel/agent/app/model" + "github.com/1Panel-dev/1Panel/agent/buserr" + "github.com/1Panel-dev/1Panel/agent/constant" + "github.com/1Panel-dev/1Panel/agent/utils/files" + "github.com/spf13/afero" +) + +type FavoriteService struct { +} + +type IFavoriteService interface { + Page(req dto.PageInfo) (int64, []response.FavoriteDTO, error) + Create(req request.FavoriteCreate) (*model.Favorite, error) + Delete(id uint) error +} + +func NewIFavoriteService() IFavoriteService { + return &FavoriteService{} +} + +func (f *FavoriteService) Page(req dto.PageInfo) (int64, []response.FavoriteDTO, error) { + total, favorites, err := favoriteRepo.Page(req.Page, req.PageSize) + if err != nil { + return 0, nil, err + } + var dtoFavorites []response.FavoriteDTO + for _, favorite := range favorites { + dtoFavorites = append(dtoFavorites, response.FavoriteDTO{ + Favorite: favorite, + }) + } + return total, dtoFavorites, nil +} + +func (f *FavoriteService) Create(req request.FavoriteCreate) (*model.Favorite, error) { + exist, _ := favoriteRepo.GetFirst(favoriteRepo.WithByPath(req.Path)) + if exist.ID > 0 { + return nil, buserr.New(constant.ErrFavoriteExist) + } + op := files.NewFileOp() + if !op.Stat(req.Path) { + return nil, buserr.New(constant.ErrLinkPathNotFound) + } + openFile, err := op.OpenFile(req.Path) + if err != nil { + return nil, err + } + fileInfo, err := openFile.Stat() + if err != nil { + return nil, err + } + favorite := &model.Favorite{ + Name: fileInfo.Name(), + IsDir: fileInfo.IsDir(), + Path: req.Path, + } + if fileInfo.Size() <= 10*1024*1024 { + afs := &afero.Afero{Fs: op.Fs} + cByte, err := afs.ReadFile(req.Path) + if err == nil { + if len(cByte) > 0 && !files.DetectBinary(cByte) { + favorite.IsTxt = true + } + } + } + if err := favoriteRepo.Create(favorite); err != nil { + return nil, err + } + return favorite, nil +} + +func (f *FavoriteService) Delete(id uint) error { + if err := favoriteRepo.Delete(commonRepo.WithByID(id)); err != nil { + return err + } + return nil +} diff --git a/agent/app/service/file.go b/agent/app/service/file.go new file mode 100644 index 000000000..869ee7ec4 --- /dev/null +++ b/agent/app/service/file.go @@ -0,0 +1,484 @@ +package service + +import ( + "fmt" + "io" + "io/fs" + "os" + "os/exec" + "path" + "path/filepath" + "strings" + "time" + "unicode/utf8" + + "github.com/1Panel-dev/1Panel/agent/app/dto/request" + "github.com/1Panel-dev/1Panel/agent/app/dto/response" + "github.com/1Panel-dev/1Panel/agent/buserr" + "github.com/1Panel-dev/1Panel/agent/constant" + "golang.org/x/net/html/charset" + "golang.org/x/sys/unix" + "golang.org/x/text/encoding/simplifiedchinese" + "golang.org/x/text/transform" + + "github.com/1Panel-dev/1Panel/agent/global" + "github.com/1Panel-dev/1Panel/agent/utils/common" + "github.com/1Panel-dev/1Panel/agent/utils/files" + "github.com/pkg/errors" +) + +type FileService struct { +} + +type IFileService interface { + GetFileList(op request.FileOption) (response.FileInfo, error) + SearchUploadWithPage(req request.SearchUploadWithPage) (int64, interface{}, error) + GetFileTree(op request.FileOption) ([]response.FileTree, error) + Create(op request.FileCreate) error + Delete(op request.FileDelete) error + BatchDelete(op request.FileBatchDelete) error + Compress(c request.FileCompress) error + DeCompress(c request.FileDeCompress) error + GetContent(op request.FileContentReq) (response.FileInfo, error) + SaveContent(edit request.FileEdit) error + FileDownload(d request.FileDownload) (string, error) + DirSize(req request.DirSizeReq) (response.DirSizeRes, error) + ChangeName(req request.FileRename) error + Wget(w request.FileWget) (string, error) + MvFile(m request.FileMove) error + ChangeOwner(req request.FileRoleUpdate) error + ChangeMode(op request.FileCreate) error + BatchChangeModeAndOwner(op request.FileRoleReq) error + ReadLogByLine(req request.FileReadByLineReq) (*response.FileLineContent, error) +} + +var filteredPaths = []string{ + "/.1panel_clash", +} + +func NewIFileService() IFileService { + return &FileService{} +} + +func (f *FileService) GetFileList(op request.FileOption) (response.FileInfo, error) { + var fileInfo response.FileInfo + if _, err := os.Stat(op.Path); err != nil && os.IsNotExist(err) { + return fileInfo, nil + } + info, err := files.NewFileInfo(op.FileOption) + if err != nil { + return fileInfo, err + } + fileInfo.FileInfo = *info + return fileInfo, nil +} + +func (f *FileService) SearchUploadWithPage(req request.SearchUploadWithPage) (int64, interface{}, error) { + var ( + files []response.UploadInfo + backData []response.UploadInfo + ) + _ = filepath.Walk(req.Path, func(path string, info os.FileInfo, err error) error { + if err != nil { + return nil + } + if !info.IsDir() { + files = append(files, response.UploadInfo{ + CreatedAt: info.ModTime().Format(constant.DateTimeLayout), + Size: int(info.Size()), + Name: info.Name(), + }) + } + return nil + }) + total, start, end := len(files), (req.Page-1)*req.PageSize, req.Page*req.PageSize + if start > total { + backData = make([]response.UploadInfo, 0) + } else { + if end >= total { + end = total + } + backData = files[start:end] + } + return int64(total), backData, nil +} + +func (f *FileService) GetFileTree(op request.FileOption) ([]response.FileTree, error) { + var treeArray []response.FileTree + if _, err := os.Stat(op.Path); err != nil && os.IsNotExist(err) { + return treeArray, nil + } + info, err := files.NewFileInfo(op.FileOption) + if err != nil { + return nil, err + } + node := response.FileTree{ + ID: common.GetUuid(), + Name: info.Name, + Path: info.Path, + IsDir: info.IsDir, + Extension: info.Extension, + } + err = f.buildFileTree(&node, info.Items, op, 2) + if err != nil { + return nil, err + } + return append(treeArray, node), nil +} + +func shouldFilterPath(path string) bool { + cleanedPath := filepath.Clean(path) + for _, filteredPath := range filteredPaths { + cleanedFilteredPath := filepath.Clean(filteredPath) + if cleanedFilteredPath == cleanedPath || strings.HasPrefix(cleanedPath, cleanedFilteredPath+"/") { + return true + } + } + return false +} + +// 递归构建文件树(只取当前目录以及当前目录下的第一层子节点) +func (f *FileService) buildFileTree(node *response.FileTree, items []*files.FileInfo, op request.FileOption, level int) error { + for _, v := range items { + if shouldFilterPath(v.Path) { + global.LOG.Infof("File Tree: Skipping %s due to filter\n", v.Path) + continue + } + childNode := response.FileTree{ + ID: common.GetUuid(), + Name: v.Name, + Path: v.Path, + IsDir: v.IsDir, + Extension: v.Extension, + } + if level > 1 && v.IsDir { + if err := f.buildChildNode(&childNode, v, op, level); err != nil { + return err + } + } + + node.Children = append(node.Children, childNode) + } + return nil +} + +func (f *FileService) buildChildNode(childNode *response.FileTree, fileInfo *files.FileInfo, op request.FileOption, level int) error { + op.Path = fileInfo.Path + subInfo, err := files.NewFileInfo(op.FileOption) + if err != nil { + if os.IsPermission(err) || errors.Is(err, unix.EACCES) { + global.LOG.Infof("File Tree: Skipping %s due to permission denied\n", fileInfo.Path) + return nil + } + global.LOG.Errorf("File Tree: Skipping %s due to error: %s\n", fileInfo.Path, err.Error()) + return nil + } + + return f.buildFileTree(childNode, subInfo.Items, op, level-1) +} + +func (f *FileService) Create(op request.FileCreate) error { + if files.IsInvalidChar(op.Path) { + return buserr.New("ErrInvalidChar") + } + fo := files.NewFileOp() + if fo.Stat(op.Path) { + return buserr.New(constant.ErrFileIsExist) + } + mode := op.Mode + if mode == 0 { + fileInfo, err := os.Stat(filepath.Dir(op.Path)) + if err == nil { + mode = int64(fileInfo.Mode().Perm()) + } else { + mode = 0755 + } + } + if op.IsDir { + return fo.CreateDirWithMode(op.Path, fs.FileMode(mode)) + } + if op.IsLink { + if !fo.Stat(op.LinkPath) { + return buserr.New(constant.ErrLinkPathNotFound) + } + return fo.LinkFile(op.LinkPath, op.Path, op.IsSymlink) + } + return fo.CreateFileWithMode(op.Path, fs.FileMode(mode)) +} + +func (f *FileService) Delete(op request.FileDelete) error { + fo := files.NewFileOp() + recycleBinStatus, _ := settingRepo.Get(settingRepo.WithByKey("FileRecycleBin")) + if recycleBinStatus.Value == "disable" { + op.ForceDelete = true + } + if op.ForceDelete { + if op.IsDir { + return fo.DeleteDir(op.Path) + } else { + return fo.DeleteFile(op.Path) + } + } + if err := NewIRecycleBinService().Create(request.RecycleBinCreate{SourcePath: op.Path}); err != nil { + return err + } + return favoriteRepo.Delete(favoriteRepo.WithByPath(op.Path)) +} + +func (f *FileService) BatchDelete(op request.FileBatchDelete) error { + fo := files.NewFileOp() + if op.IsDir { + for _, file := range op.Paths { + if err := fo.DeleteDir(file); err != nil { + return err + } + } + } else { + for _, file := range op.Paths { + if err := fo.DeleteFile(file); err != nil { + return err + } + } + } + return nil +} + +func (f *FileService) ChangeMode(op request.FileCreate) error { + fo := files.NewFileOp() + return fo.ChmodR(op.Path, op.Mode, op.Sub) +} + +func (f *FileService) BatchChangeModeAndOwner(op request.FileRoleReq) error { + fo := files.NewFileOp() + for _, path := range op.Paths { + if !fo.Stat(path) { + return buserr.New(constant.ErrPathNotFound) + } + if err := fo.ChownR(path, op.User, op.Group, op.Sub); err != nil { + return err + } + if err := fo.ChmodR(path, op.Mode, op.Sub); err != nil { + return err + } + } + return nil + +} + +func (f *FileService) ChangeOwner(req request.FileRoleUpdate) error { + fo := files.NewFileOp() + return fo.ChownR(req.Path, req.User, req.Group, req.Sub) +} + +func (f *FileService) Compress(c request.FileCompress) error { + fo := files.NewFileOp() + if !c.Replace && fo.Stat(filepath.Join(c.Dst, c.Name)) { + return buserr.New(constant.ErrFileIsExist) + } + return fo.Compress(c.Files, c.Dst, c.Name, files.CompressType(c.Type), c.Secret) +} + +func (f *FileService) DeCompress(c request.FileDeCompress) error { + fo := files.NewFileOp() + if c.Type == "tar" && len(c.Secret) != 0 { + c.Type = "tar.gz" + } + return fo.Decompress(c.Path, c.Dst, files.CompressType(c.Type), c.Secret) +} + +func (f *FileService) GetContent(op request.FileContentReq) (response.FileInfo, error) { + info, err := files.NewFileInfo(files.FileOption{ + Path: op.Path, + Expand: true, + IsDetail: op.IsDetail, + }) + if err != nil { + return response.FileInfo{}, err + } + + content := []byte(info.Content) + if len(content) > 1024 { + content = content[:1024] + } + if !utf8.Valid(content) { + _, decodeName, _ := charset.DetermineEncoding(content, "") + if decodeName == "windows-1252" { + reader := strings.NewReader(info.Content) + item := transform.NewReader(reader, simplifiedchinese.GBK.NewDecoder()) + contents, err := io.ReadAll(item) + if err != nil { + return response.FileInfo{}, err + } + info.Content = string(contents) + } + } + return response.FileInfo{FileInfo: *info}, nil +} + +func (f *FileService) SaveContent(edit request.FileEdit) error { + info, err := files.NewFileInfo(files.FileOption{ + Path: edit.Path, + Expand: false, + }) + if err != nil { + return err + } + + fo := files.NewFileOp() + return fo.WriteFile(edit.Path, strings.NewReader(edit.Content), info.FileMode) +} + +func (f *FileService) ChangeName(req request.FileRename) error { + if files.IsInvalidChar(req.NewName) { + return buserr.New("ErrInvalidChar") + } + fo := files.NewFileOp() + return fo.Rename(req.OldName, req.NewName) +} + +func (f *FileService) Wget(w request.FileWget) (string, error) { + fo := files.NewFileOp() + key := "file-wget-" + common.GetUuid() + return key, fo.DownloadFileWithProcess(w.Url, filepath.Join(w.Path, w.Name), key, w.IgnoreCertificate) +} + +func (f *FileService) MvFile(m request.FileMove) error { + fo := files.NewFileOp() + if !fo.Stat(m.NewPath) { + return buserr.New(constant.ErrPathNotFound) + } + for _, oldPath := range m.OldPaths { + if !fo.Stat(oldPath) { + return buserr.WithName(constant.ErrFileNotFound, oldPath) + } + if oldPath == m.NewPath || strings.Contains(m.NewPath, filepath.Clean(oldPath)+"/") { + return buserr.New(constant.ErrMovePathFailed) + } + } + if m.Type == "cut" { + return fo.Cut(m.OldPaths, m.NewPath, m.Name, m.Cover) + } + var errs []error + if m.Type == "copy" { + for _, src := range m.OldPaths { + if err := fo.CopyAndReName(src, m.NewPath, m.Name, m.Cover); err != nil { + errs = append(errs, err) + global.LOG.Errorf("copy file [%s] to [%s] failed, err: %s", src, m.NewPath, err.Error()) + } + } + } + + var errString string + for _, err := range errs { + errString += err.Error() + "\n" + } + if errString != "" { + return errors.New(errString) + } + return nil +} + +func (f *FileService) FileDownload(d request.FileDownload) (string, error) { + filePath := d.Paths[0] + if d.Compress { + tempPath := filepath.Join(os.TempDir(), fmt.Sprintf("%d", time.Now().UnixNano())) + if err := os.MkdirAll(tempPath, os.ModePerm); err != nil { + return "", err + } + fo := files.NewFileOp() + if err := fo.Compress(d.Paths, tempPath, d.Name, files.CompressType(d.Type), ""); err != nil { + return "", err + } + filePath = filepath.Join(tempPath, d.Name) + } + return filePath, nil +} + +func (f *FileService) DirSize(req request.DirSizeReq) (response.DirSizeRes, error) { + var ( + res response.DirSizeRes + ) + if req.Path == "/proc" { + return res, nil + } + cmd := exec.Command("du", "-s", req.Path) + output, err := cmd.Output() + if err == nil { + fields := strings.Fields(string(output)) + if len(fields) == 2 { + var cmdSize int64 + _, err = fmt.Sscanf(fields[0], "%d", &cmdSize) + if err == nil { + res.Size = float64(cmdSize * 1024) + return res, nil + } + } + } + fo := files.NewFileOp() + size, err := fo.GetDirSize(req.Path) + if err != nil { + return res, err + } + res.Size = size + return res, nil +} + +func (f *FileService) ReadLogByLine(req request.FileReadByLineReq) (*response.FileLineContent, error) { + logFilePath := "" + switch req.Type { + case constant.TypeWebsite: + website, err := websiteRepo.GetFirst(commonRepo.WithByID(req.ID)) + if err != nil { + return nil, err + } + nginx, err := getNginxFull(&website) + if err != nil { + return nil, err + } + sitePath := path.Join(nginx.SiteDir, "sites", website.Alias) + logFilePath = path.Join(sitePath, "log", req.Name) + case constant.TypePhp: + php, err := runtimeRepo.GetFirst(commonRepo.WithByID(req.ID)) + if err != nil { + return nil, err + } + logFilePath = php.GetLogPath() + case constant.TypeSSL: + ssl, err := websiteSSLRepo.GetFirst(commonRepo.WithByID(req.ID)) + if err != nil { + return nil, err + } + logFilePath = ssl.GetLogPath() + case constant.TypeSystem: + fileName := "" + if len(req.Name) == 0 || req.Name == time.Now().Format("2006-01-02") { + fileName = "1Panel.log" + } else { + fileName = "1Panel-" + req.Name + ".log" + } + logFilePath = path.Join(global.CONF.System.DataDir, "log", fileName) + if _, err := os.Stat(logFilePath); err != nil { + fileGzPath := path.Join(global.CONF.System.DataDir, "log", fileName+".gz") + if _, err := os.Stat(fileGzPath); err != nil { + return nil, buserr.New("ErrHttpReqNotFound") + } + if err := handleGunzip(fileGzPath); err != nil { + return nil, fmt.Errorf("handle ungzip file %s failed, err: %v", fileGzPath, err) + } + } + case "image-pull", "image-push", "image-build", "compose-create": + logFilePath = path.Join(global.CONF.System.TmpDir, fmt.Sprintf("docker_logs/%s", req.Name)) + } + + lines, isEndOfFile, total, err := files.ReadFileByLine(logFilePath, req.Page, req.PageSize, req.Latest) + if err != nil { + return nil, err + } + res := &response.FileLineContent{ + Content: strings.Join(lines, "\n"), + End: isEndOfFile, + Path: logFilePath, + Total: total, + } + return res, nil +} diff --git a/agent/app/service/firewall.go b/agent/app/service/firewall.go new file mode 100644 index 000000000..6bcd7a41b --- /dev/null +++ b/agent/app/service/firewall.go @@ -0,0 +1,694 @@ +package service + +import ( + "fmt" + "os" + "sort" + "strconv" + "strings" + "sync" + + "github.com/1Panel-dev/1Panel/agent/app/dto" + "github.com/1Panel-dev/1Panel/agent/app/model" + "github.com/1Panel-dev/1Panel/agent/constant" + "github.com/1Panel-dev/1Panel/agent/global" + "github.com/1Panel-dev/1Panel/agent/utils/cmd" + "github.com/1Panel-dev/1Panel/agent/utils/common" + "github.com/1Panel-dev/1Panel/agent/utils/firewall" + fireClient "github.com/1Panel-dev/1Panel/agent/utils/firewall/client" + "github.com/jinzhu/copier" + "github.com/pkg/errors" +) + +const confPath = "/etc/sysctl.conf" + +type FirewallService struct{} + +type IFirewallService interface { + LoadBaseInfo() (dto.FirewallBaseInfo, error) + SearchWithPage(search dto.RuleSearch) (int64, interface{}, error) + OperateFirewall(operation string) error + OperatePortRule(req dto.PortRuleOperate, reload bool) error + OperateForwardRule(req dto.ForwardRuleOperate) error + OperateAddressRule(req dto.AddrRuleOperate, reload bool) error + UpdatePortRule(req dto.PortRuleUpdate) error + UpdateAddrRule(req dto.AddrRuleUpdate) error + UpdateDescription(req dto.UpdateFirewallDescription) error + BatchOperateRule(req dto.BatchRuleOperate) error +} + +func NewIFirewallService() IFirewallService { + return &FirewallService{} +} + +func (u *FirewallService) LoadBaseInfo() (dto.FirewallBaseInfo, error) { + var baseInfo dto.FirewallBaseInfo + baseInfo.Status = "not running" + baseInfo.Version = "-" + baseInfo.Name = "-" + client, err := firewall.NewFirewallClient() + if err != nil { + return baseInfo, err + } + baseInfo.Name = client.Name() + + var wg sync.WaitGroup + wg.Add(3) + go func() { + defer wg.Done() + baseInfo.PingStatus = u.pingStatus() + }() + go func() { + defer wg.Done() + baseInfo.Status, _ = client.Status() + }() + go func() { + defer wg.Done() + baseInfo.Version, _ = client.Version() + }() + wg.Wait() + return baseInfo, nil +} + +func (u *FirewallService) SearchWithPage(req dto.RuleSearch) (int64, interface{}, error) { + var ( + datas []fireClient.FireInfo + backDatas []fireClient.FireInfo + ) + + client, err := firewall.NewFirewallClient() + if err != nil { + return 0, nil, err + } + + var rules []fireClient.FireInfo + switch req.Type { + case "port": + rules, err = client.ListPort() + case "forward": + rules, err = client.ListForward() + case "address": + rules, err = client.ListAddress() + } + if err != nil { + return 0, nil, err + } + + if len(req.Info) != 0 { + for _, addr := range rules { + if strings.Contains(addr.Address, req.Info) || + strings.Contains(addr.Port, req.Info) || + strings.Contains(addr.TargetPort, req.Info) || + strings.Contains(addr.TargetIP, req.Info) { + datas = append(datas, addr) + } + } + } else { + datas = rules + } + if req.Type == "port" { + apps := u.loadPortByApp() + for i := 0; i < len(datas); i++ { + datas[i].UsedStatus = checkPortUsed(datas[i].Port, datas[i].Protocol, apps) + } + } + + var datasFilterStatus []fireClient.FireInfo + if len(req.Status) != 0 { + for _, data := range datas { + if req.Status == "free" && len(data.UsedStatus) == 0 { + datasFilterStatus = append(datasFilterStatus, data) + } + if req.Status == "used" && len(data.UsedStatus) != 0 { + datasFilterStatus = append(datasFilterStatus, data) + } + } + } else { + datasFilterStatus = datas + } + + var datasFilterStrategy []fireClient.FireInfo + if len(req.Strategy) != 0 { + for _, data := range datasFilterStatus { + if req.Strategy == data.Strategy { + datasFilterStrategy = append(datasFilterStrategy, data) + } + } + } else { + datasFilterStrategy = datasFilterStatus + } + + total, start, end := len(datasFilterStrategy), (req.Page-1)*req.PageSize, req.Page*req.PageSize + if start > total { + backDatas = make([]fireClient.FireInfo, 0) + } else { + if end >= total { + end = total + } + backDatas = datasFilterStrategy[start:end] + } + + datasFromDB, _ := hostRepo.ListFirewallRecord() + for i := 0; i < len(backDatas); i++ { + for _, des := range datasFromDB { + if req.Type != des.Type { + continue + } + if backDatas[i].Port == des.Port && + req.Type == "port" && + backDatas[i].Protocol == des.Protocol && + backDatas[i].Strategy == des.Strategy && + backDatas[i].Address == des.Address { + backDatas[i].Description = des.Description + break + } + if req.Type == "address" && backDatas[i].Strategy == des.Strategy && backDatas[i].Address == des.Address { + backDatas[i].Description = des.Description + break + } + } + } + + go u.cleanUnUsedData(client) + + return int64(total), backDatas, nil +} + +func (u *FirewallService) OperateFirewall(operation string) error { + client, err := firewall.NewFirewallClient() + if err != nil { + return err + } + switch operation { + case "start": + if err := client.Start(); err != nil { + return err + } + if err := u.addPortsBeforeStart(client); err != nil { + _ = client.Stop() + return err + } + _, _ = cmd.Exec("systemctl restart docker") + return nil + case "stop": + if err := client.Stop(); err != nil { + return err + } + _, _ = cmd.Exec("systemctl restart docker") + return nil + case "restart": + if err := client.Restart(); err != nil { + return err + } + _, _ = cmd.Exec("systemctl restart docker") + return nil + case "disablePing": + return u.updatePingStatus("0") + case "enablePing": + return u.updatePingStatus("1") + } + return fmt.Errorf("not support such operation: %s", operation) +} + +func (u *FirewallService) OperatePortRule(req dto.PortRuleOperate, reload bool) error { + client, err := firewall.NewFirewallClient() + if err != nil { + return err + } + protos := strings.Split(req.Protocol, "/") + itemAddress := strings.Split(strings.TrimSuffix(req.Address, ","), ",") + + if client.Name() == "ufw" { + if strings.Contains(req.Port, ",") || strings.Contains(req.Port, "-") { + for _, proto := range protos { + for _, addr := range itemAddress { + if len(addr) == 0 { + addr = "Anywhere" + } + req.Address = addr + req.Port = strings.ReplaceAll(req.Port, "-", ":") + req.Protocol = proto + if err := u.operatePort(client, req); err != nil { + return err + } + req.Port = strings.ReplaceAll(req.Port, ":", "-") + if err := u.addPortRecord(req); err != nil { + return err + } + } + } + return nil + } + for _, addr := range itemAddress { + if len(addr) == 0 { + addr = "Anywhere" + } + if req.Protocol == "tcp/udp" { + req.Protocol = "" + } + req.Address = addr + if err := u.operatePort(client, req); err != nil { + return err + } + if len(req.Protocol) == 0 { + req.Protocol = "tcp/udp" + } + if err := u.addPortRecord(req); err != nil { + return err + } + } + return nil + } + + itemPorts := req.Port + for _, proto := range protos { + if strings.Contains(req.Port, "-") { + for _, addr := range itemAddress { + req.Protocol = proto + req.Address = addr + if err := u.operatePort(client, req); err != nil { + return err + } + if err := u.addPortRecord(req); err != nil { + return err + } + } + } else { + ports := strings.Split(itemPorts, ",") + for _, port := range ports { + if len(port) == 0 { + continue + } + for _, addr := range itemAddress { + req.Address = addr + req.Port = port + req.Protocol = proto + if err := u.operatePort(client, req); err != nil { + return err + } + if err := u.addPortRecord(req); err != nil { + return err + } + } + } + } + } + + if reload { + return client.Reload() + } + return nil +} + +func (u *FirewallService) OperateForwardRule(req dto.ForwardRuleOperate) error { + client, err := firewall.NewFirewallClient() + if err != nil { + return err + } + + rules, _ := client.ListForward() + for _, rule := range rules { + for _, reqRule := range req.Rules { + if reqRule.Operation == "remove" { + continue + } + if reqRule.TargetIP == "" { + reqRule.TargetIP = "127.0.0.1" + } + if reqRule.Port == rule.Port && reqRule.TargetPort == rule.TargetPort && reqRule.TargetIP == rule.TargetIP { + return constant.ErrRecordExist + } + } + } + + sort.SliceStable(req.Rules, func(i, j int) bool { + n1, _ := strconv.Atoi(req.Rules[i].Num) + n2, _ := strconv.Atoi(req.Rules[j].Num) + return n1 > n2 + }) + + for _, r := range req.Rules { + for _, p := range strings.Split(r.Protocol, "/") { + if r.TargetIP == "" { + r.TargetIP = "127.0.0.1" + } + if err = client.PortForward(fireClient.Forward{ + Num: r.Num, + Protocol: p, + Port: r.Port, + TargetIP: r.TargetIP, + TargetPort: r.TargetPort, + }, r.Operation); err != nil { + return err + } + } + } + return nil +} + +func (u *FirewallService) OperateAddressRule(req dto.AddrRuleOperate, reload bool) error { + client, err := firewall.NewFirewallClient() + if err != nil { + return err + } + + var fireInfo fireClient.FireInfo + if err := copier.Copy(&fireInfo, &req); err != nil { + return err + } + + addressList := strings.Split(req.Address, ",") + for i := 0; i < len(addressList); i++ { + if len(addressList[i]) == 0 { + continue + } + fireInfo.Address = addressList[i] + if err := client.RichRules(fireInfo, req.Operation); err != nil { + return err + } + req.Address = addressList[i] + if err := u.addAddressRecord(req); err != nil { + return err + } + } + if reload { + return client.Reload() + } + return nil +} + +func (u *FirewallService) UpdatePortRule(req dto.PortRuleUpdate) error { + client, err := firewall.NewFirewallClient() + if err != nil { + return err + } + if err := u.OperatePortRule(req.OldRule, false); err != nil { + return err + } + if err := u.OperatePortRule(req.NewRule, false); err != nil { + return err + } + return client.Reload() +} + +func (u *FirewallService) UpdateAddrRule(req dto.AddrRuleUpdate) error { + client, err := firewall.NewFirewallClient() + if err != nil { + return err + } + if err := u.OperateAddressRule(req.OldRule, false); err != nil { + return err + } + if err := u.OperateAddressRule(req.NewRule, false); err != nil { + return err + } + return client.Reload() +} + +func (u *FirewallService) UpdateDescription(req dto.UpdateFirewallDescription) error { + var firewall model.Firewall + if err := copier.Copy(&firewall, &req); err != nil { + return errors.WithMessage(constant.ErrStructTransform, err.Error()) + } + return hostRepo.SaveFirewallRecord(&firewall) +} + +func (u *FirewallService) BatchOperateRule(req dto.BatchRuleOperate) error { + client, err := firewall.NewFirewallClient() + if err != nil { + return err + } + if req.Type == "port" { + for _, rule := range req.Rules { + _ = u.OperatePortRule(rule, false) + } + return client.Reload() + } + for _, rule := range req.Rules { + itemRule := dto.AddrRuleOperate{Operation: rule.Operation, Address: rule.Address, Strategy: rule.Strategy} + _ = u.OperateAddressRule(itemRule, false) + } + return client.Reload() +} + +func OperateFirewallPort(oldPorts, newPorts []int) error { + client, err := firewall.NewFirewallClient() + if err != nil { + return err + } + for _, port := range newPorts { + if err := client.Port(fireClient.FireInfo{Port: strconv.Itoa(port), Protocol: "tcp", Strategy: "accept"}, "add"); err != nil { + return err + } + } + for _, port := range oldPorts { + if err := client.Port(fireClient.FireInfo{Port: strconv.Itoa(port), Protocol: "tcp", Strategy: "accept"}, "remove"); err != nil { + return err + } + } + return client.Reload() +} + +func (u *FirewallService) operatePort(client firewall.FirewallClient, req dto.PortRuleOperate) error { + var fireInfo fireClient.FireInfo + if err := copier.Copy(&fireInfo, &req); err != nil { + return err + } + + if client.Name() == "ufw" { + if len(fireInfo.Address) != 0 && !strings.EqualFold(fireInfo.Address, "Anywhere") { + return client.RichRules(fireInfo, req.Operation) + } + return client.Port(fireInfo, req.Operation) + } + + if len(fireInfo.Address) != 0 || fireInfo.Strategy == "drop" { + return client.RichRules(fireInfo, req.Operation) + } + return client.Port(fireInfo, req.Operation) +} + +type portOfApp struct { + AppName string + HttpPort string + HttpsPort string +} + +func (u *FirewallService) loadPortByApp() []portOfApp { + var datas []portOfApp + apps, err := appInstallRepo.ListBy() + if err != nil { + return datas + } + for i := 0; i < len(apps); i++ { + datas = append(datas, portOfApp{ + AppName: apps[i].App.Key, + HttpPort: strconv.Itoa(apps[i].HttpPort), + HttpsPort: strconv.Itoa(apps[i].HttpsPort), + }) + } + systemPort, err := settingRepo.Get(settingRepo.WithByKey("ServerPort")) + if err != nil { + return datas + } + datas = append(datas, portOfApp{AppName: "1panel", HttpPort: systemPort.Value}) + + return datas +} + +func (u *FirewallService) cleanUnUsedData(client firewall.FirewallClient) { + list, _ := client.ListPort() + addressList, _ := client.ListAddress() + list = append(list, addressList...) + if len(list) == 0 { + return + } + records, _ := hostRepo.ListFirewallRecord() + if len(records) == 0 { + return + } + for _, item := range list { + for i := 0; i < len(records); i++ { + if records[i].Port == item.Port && records[i].Protocol == item.Protocol && records[i].Strategy == item.Strategy && records[i].Address == item.Address { + records = append(records[:i], records[i+1:]...) + } + } + } + + for _, record := range records { + _ = hostRepo.DeleteFirewallRecordByID(record.ID) + } +} +func (u *FirewallService) pingStatus() string { + if _, err := os.Stat("/etc/sysctl.conf"); err != nil { + return constant.StatusNone + } + sudo := cmd.SudoHandleCmd() + command := fmt.Sprintf("%s cat /etc/sysctl.conf | grep net/ipv4/icmp_echo_ignore_all= ", sudo) + stdout, _ := cmd.Exec(command) + if stdout == "net/ipv4/icmp_echo_ignore_all=1\n" { + return constant.StatusEnable + } + return constant.StatusDisable +} + +func (u *FirewallService) updatePingStatus(enable string) error { + lineBytes, err := os.ReadFile(confPath) + if err != nil { + return err + } + files := strings.Split(string(lineBytes), "\n") + var newFiles []string + hasLine := false + for _, line := range files { + if strings.Contains(line, "net/ipv4/icmp_echo_ignore_all") || strings.HasPrefix(line, "net/ipv4/icmp_echo_ignore_all") { + newFiles = append(newFiles, "net/ipv4/icmp_echo_ignore_all="+enable) + hasLine = true + } else { + newFiles = append(newFiles, line) + } + } + if !hasLine { + newFiles = append(newFiles, "net/ipv4/icmp_echo_ignore_all="+enable) + } + file, err := os.OpenFile(confPath, os.O_WRONLY|os.O_TRUNC, 0666) + if err != nil { + return err + } + defer file.Close() + _, err = file.WriteString(strings.Join(newFiles, "\n")) + if err != nil { + return err + } + + sudo := cmd.SudoHandleCmd() + command := fmt.Sprintf("%s sysctl -p", sudo) + stdout, err := cmd.Exec(command) + if err != nil { + return fmt.Errorf("update ping status failed, err: %v", stdout) + } + + return nil +} + +func (u *FirewallService) addPortsBeforeStart(client firewall.FirewallClient) error { + serverPort, err := settingRepo.Get(settingRepo.WithByKey("ServerPort")) + if err != nil { + return err + } + if err := client.Port(fireClient.FireInfo{Port: serverPort.Value, Protocol: "tcp", Strategy: "accept"}, "add"); err != nil { + return err + } + if err := client.Port(fireClient.FireInfo{Port: "22", Protocol: "tcp", Strategy: "accept"}, "add"); err != nil { + return err + } + if err := client.Port(fireClient.FireInfo{Port: "80", Protocol: "tcp", Strategy: "accept"}, "add"); err != nil { + return err + } + if err := client.Port(fireClient.FireInfo{Port: "443", Protocol: "tcp", Strategy: "accept"}, "add"); err != nil { + return err + } + apps := u.loadPortByApp() + for _, app := range apps { + if len(app.HttpPort) != 0 && app.HttpPort != "0" { + if err := client.Port(fireClient.FireInfo{Port: app.HttpPort, Protocol: "tcp", Strategy: "accept"}, "add"); err != nil { + return err + } + } + } + + return client.Reload() +} + +func (u *FirewallService) addPortRecord(req dto.PortRuleOperate) error { + if req.Operation == "remove" { + return hostRepo.DeleteFirewallRecord("port", req.Port, req.Protocol, req.Address, req.Strategy) + } + + if err := hostRepo.SaveFirewallRecord(&model.Firewall{ + Type: "port", + Port: req.Port, + Protocol: req.Protocol, + Address: req.Address, + Strategy: req.Strategy, + Description: req.Description, + }); err != nil { + return fmt.Errorf("add record %s/%s failed (strategy: %s, address: %s), err: %v", req.Port, req.Protocol, req.Strategy, req.Address, err) + } + + return nil +} + +func (u *FirewallService) addAddressRecord(req dto.AddrRuleOperate) error { + if req.Operation == "remove" { + return hostRepo.DeleteFirewallRecord("address", "", "", req.Address, req.Strategy) + } + if err := hostRepo.SaveFirewallRecord(&model.Firewall{ + Type: "address", + Address: req.Address, + Strategy: req.Strategy, + Description: req.Description, + }); err != nil { + return fmt.Errorf("add record failed (strategy: %s, address: %s), err: %v", req.Strategy, req.Address, err) + } + return nil +} + +func checkPortUsed(ports, proto string, apps []portOfApp) string { + var portList []int + if strings.Contains(ports, "-") || strings.Contains(ports, ",") { + if strings.Contains(ports, "-") { + port1, err := strconv.Atoi(strings.Split(ports, "-")[0]) + if err != nil { + global.LOG.Errorf(" convert string %s to int failed, err: %v", strings.Split(ports, "-")[0], err) + return "" + } + port2, err := strconv.Atoi(strings.Split(ports, "-")[1]) + if err != nil { + global.LOG.Errorf(" convert string %s to int failed, err: %v", strings.Split(ports, "-")[1], err) + return "" + } + for i := port1; i <= port2; i++ { + portList = append(portList, i) + } + } else { + portLists := strings.Split(ports, ",") + for _, item := range portLists { + portItem, _ := strconv.Atoi(item) + portList = append(portList, portItem) + } + } + + var usedPorts []string + for _, port := range portList { + portItem := fmt.Sprintf("%v", port) + isUsedByApp := false + for _, app := range apps { + if app.HttpPort == portItem || app.HttpsPort == portItem { + isUsedByApp = true + usedPorts = append(usedPorts, fmt.Sprintf("%s (%s)", portItem, app.AppName)) + break + } + } + if !isUsedByApp && common.ScanPortWithProto(port, proto) { + usedPorts = append(usedPorts, fmt.Sprintf("%v", port)) + } + } + return strings.Join(usedPorts, ",") + } + + for _, app := range apps { + if app.HttpPort == ports || app.HttpsPort == ports { + return fmt.Sprintf("(%s)", app.AppName) + } + } + port, err := strconv.Atoi(ports) + if err != nil { + global.LOG.Errorf(" convert string %v to int failed, err: %v", port, err) + return "" + } + if common.ScanPortWithProto(port, proto) { + return "inUsed" + } + return "" +} diff --git a/agent/app/service/ftp.go b/agent/app/service/ftp.go new file mode 100644 index 000000000..d0dfef158 --- /dev/null +++ b/agent/app/service/ftp.go @@ -0,0 +1,245 @@ +package service + +import ( + "os" + "sort" + + "github.com/1Panel-dev/1Panel/agent/app/dto" + "github.com/1Panel-dev/1Panel/agent/app/model" + "github.com/1Panel-dev/1Panel/agent/constant" + "github.com/1Panel-dev/1Panel/agent/utils/encrypt" + "github.com/1Panel-dev/1Panel/agent/utils/toolbox" + "github.com/jinzhu/copier" + "github.com/pkg/errors" +) + +type FtpService struct{} + +type IFtpService interface { + LoadBaseInfo() (dto.FtpBaseInfo, error) + SearchWithPage(search dto.SearchWithPage) (int64, interface{}, error) + Operate(operation string) error + Create(req dto.FtpCreate) (uint, error) + Delete(req dto.BatchDeleteReq) error + Update(req dto.FtpUpdate) error + Sync() error + LoadLog(req dto.FtpLogSearch) (int64, interface{}, error) +} + +func NewIFtpService() IFtpService { + return &FtpService{} +} + +func (f *FtpService) LoadBaseInfo() (dto.FtpBaseInfo, error) { + var baseInfo dto.FtpBaseInfo + client, err := toolbox.NewFtpClient() + if err != nil { + return baseInfo, err + } + baseInfo.IsActive, baseInfo.IsExist = client.Status() + return baseInfo, nil +} + +func (f *FtpService) LoadLog(req dto.FtpLogSearch) (int64, interface{}, error) { + client, err := toolbox.NewFtpClient() + if err != nil { + return 0, nil, err + } + logItem, err := client.LoadLogs(req.User, req.Operation) + if err != nil { + return 0, nil, err + } + sort.Slice(logItem, func(i, j int) bool { + return logItem[i].Time > logItem[j].Time + }) + var logs []toolbox.FtpLog + total, start, end := len(logItem), (req.Page-1)*req.PageSize, req.Page*req.PageSize + if start > total { + logs = make([]toolbox.FtpLog, 0) + } else { + if end >= total { + end = total + } + logs = logItem[start:end] + } + return int64(total), logs, nil +} + +func (u *FtpService) Operate(operation string) error { + client, err := toolbox.NewFtpClient() + if err != nil { + return err + } + return client.Operate(operation) +} + +func (f *FtpService) SearchWithPage(req dto.SearchWithPage) (int64, interface{}, error) { + total, lists, err := ftpRepo.Page(req.Page, req.PageSize, ftpRepo.WithByUser(req.Info), commonRepo.WithOrderBy("created_at desc")) + if err != nil { + return 0, nil, err + } + var users []dto.FtpInfo + for _, user := range lists { + var item dto.FtpInfo + if err := copier.Copy(&item, &user); err != nil { + return 0, nil, errors.WithMessage(constant.ErrStructTransform, err.Error()) + } + item.Password, _ = encrypt.StringDecrypt(item.Password) + users = append(users, item) + } + return total, users, err +} + +func (f *FtpService) Sync() error { + client, err := toolbox.NewFtpClient() + if err != nil { + return err + } + lists, err := client.LoadList() + if err != nil { + return nil + } + listsInDB, err := ftpRepo.GetList() + if err != nil { + return err + } + sameData := make(map[string]struct{}) + for _, item := range lists { + for _, itemInDB := range listsInDB { + if item.User == itemInDB.User { + sameData[item.User] = struct{}{} + if item.Path != itemInDB.Path || item.Status != itemInDB.Status { + _ = ftpRepo.Update(itemInDB.ID, map[string]interface{}{"path": item.Path, "status": item.Status}) + } + break + } + } + } + for _, item := range lists { + if _, ok := sameData[item.User]; !ok { + _ = ftpRepo.Create(&model.Ftp{User: item.User, Path: item.Path, Status: item.Status}) + } + } + for _, item := range listsInDB { + if _, ok := sameData[item.User]; !ok { + _ = ftpRepo.Update(item.ID, map[string]interface{}{"status": "deleted"}) + } + } + return nil +} + +func (f *FtpService) Create(req dto.FtpCreate) (uint, error) { + if _, err := os.Stat(req.Path); err != nil { + if os.IsNotExist(err) { + if err := os.MkdirAll(req.Path, os.ModePerm); err != nil { + return 0, err + } + } else { + return 0, err + } + } + pass, err := encrypt.StringEncrypt(req.Password) + if err != nil { + return 0, err + } + userInDB, _ := ftpRepo.Get(hostRepo.WithByUser(req.User)) + if userInDB.ID != 0 { + return 0, constant.ErrRecordExist + } + client, err := toolbox.NewFtpClient() + if err != nil { + return 0, err + } + if err := client.UserAdd(req.User, req.Password, req.Path); err != nil { + return 0, err + } + var ftp model.Ftp + if err := copier.Copy(&ftp, &req); err != nil { + return 0, errors.WithMessage(constant.ErrStructTransform, err.Error()) + } + ftp.Status = constant.StatusEnable + ftp.Password = pass + if err := ftpRepo.Create(&ftp); err != nil { + return 0, err + } + return ftp.ID, nil +} + +func (f *FtpService) Delete(req dto.BatchDeleteReq) error { + client, err := toolbox.NewFtpClient() + if err != nil { + return err + } + for _, id := range req.Ids { + ftpItem, err := ftpRepo.Get(commonRepo.WithByID(id)) + if err != nil { + return err + } + _ = client.UserDel(ftpItem.User) + _ = ftpRepo.Delete(commonRepo.WithByID(id)) + } + return nil +} + +func (f *FtpService) Update(req dto.FtpUpdate) error { + if _, err := os.Stat(req.Path); err != nil { + if os.IsNotExist(err) { + if err := os.MkdirAll(req.Path, os.ModePerm); err != nil { + return err + } + } else { + return err + } + } + + pass, err := encrypt.StringEncrypt(req.Password) + if err != nil { + return err + } + ftpItem, _ := ftpRepo.Get(commonRepo.WithByID(req.ID)) + if ftpItem.ID == 0 { + return constant.ErrRecordNotFound + } + passItem, err := encrypt.StringDecrypt(ftpItem.Password) + if err != nil { + return err + } + + client, err := toolbox.NewFtpClient() + if err != nil { + return err + } + needReload := false + updates := make(map[string]interface{}) + if req.Password != passItem { + if err := client.SetPasswd(ftpItem.User, req.Password); err != nil { + return err + } + updates["password"] = pass + needReload = true + } + if req.Status != ftpItem.Status { + if err := client.SetStatus(ftpItem.User, req.Status); err != nil { + return err + } + updates["status"] = req.Status + needReload = true + } + if req.Path != ftpItem.Path { + if err := client.SetPath(ftpItem.User, req.Path); err != nil { + return err + } + updates["path"] = req.Path + needReload = true + } + if req.Description != ftpItem.Description { + updates["description"] = req.Description + } + if needReload { + _ = client.Reload() + } + if len(updates) != 0 { + return ftpRepo.Update(ftpItem.ID, updates) + } + return nil +} diff --git a/agent/app/service/group.go b/agent/app/service/group.go new file mode 100644 index 000000000..97ec2e2e6 --- /dev/null +++ b/agent/app/service/group.go @@ -0,0 +1,90 @@ +package service + +import ( + "github.com/1Panel-dev/1Panel/agent/app/dto" + "github.com/1Panel-dev/1Panel/agent/buserr" + "github.com/1Panel-dev/1Panel/agent/constant" + "github.com/jinzhu/copier" + "github.com/pkg/errors" +) + +type GroupService struct{} + +type IGroupService interface { + List(req dto.GroupSearch) ([]dto.GroupInfo, error) + Create(req dto.GroupCreate) error + Update(req dto.GroupUpdate) error + Delete(id uint) error +} + +func NewIGroupService() IGroupService { + return &GroupService{} +} + +func (u *GroupService) List(req dto.GroupSearch) ([]dto.GroupInfo, error) { + groups, err := groupRepo.GetList(commonRepo.WithByType(req.Type), commonRepo.WithOrderBy("is_default desc"), commonRepo.WithOrderBy("created_at desc")) + if err != nil { + return nil, constant.ErrRecordNotFound + } + var dtoUsers []dto.GroupInfo + for _, group := range groups { + var item dto.GroupInfo + if err := copier.Copy(&item, &group); err != nil { + return nil, errors.WithMessage(constant.ErrStructTransform, err.Error()) + } + dtoUsers = append(dtoUsers, item) + } + return dtoUsers, err +} + +func (u *GroupService) Create(req dto.GroupCreate) error { + group, _ := groupRepo.Get(commonRepo.WithByName(req.Name), commonRepo.WithByType(req.Type)) + if group.ID != 0 { + return constant.ErrRecordExist + } + if err := copier.Copy(&group, &req); err != nil { + return errors.WithMessage(constant.ErrStructTransform, err.Error()) + } + if err := groupRepo.Create(&group); err != nil { + return err + } + return nil +} + +func (u *GroupService) Delete(id uint) error { + group, _ := groupRepo.Get(commonRepo.WithByID(id)) + if group.ID == 0 { + return constant.ErrRecordNotFound + } + switch group.Type { + case "website": + websites, _ := websiteRepo.GetBy(websiteRepo.WithGroupID(id)) + if len(websites) > 0 { + return buserr.New(constant.ErrGroupIsUsed) + } + case "command": + commands, _ := commandRepo.GetList(commonRepo.WithByGroupID(id)) + if len(commands) > 0 { + return buserr.New(constant.ErrGroupIsUsed) + } + case "host": + hosts, _ := hostRepo.GetList(commonRepo.WithByGroupID(id)) + if len(hosts) > 0 { + return buserr.New(constant.ErrGroupIsUsed) + } + } + return groupRepo.Delete(commonRepo.WithByID(id)) +} + +func (u *GroupService) Update(req dto.GroupUpdate) error { + if req.IsDefault { + if err := groupRepo.CancelDefault(req.Type); err != nil { + return err + } + } + upMap := make(map[string]interface{}) + upMap["name"] = req.Name + upMap["is_default"] = req.IsDefault + + return groupRepo.Update(req.ID, upMap) +} diff --git a/agent/app/service/helper.go b/agent/app/service/helper.go new file mode 100644 index 000000000..725e8f78e --- /dev/null +++ b/agent/app/service/helper.go @@ -0,0 +1,15 @@ +package service + +import ( + "context" + + "github.com/1Panel-dev/1Panel/agent/constant" + "github.com/1Panel-dev/1Panel/agent/global" + "gorm.io/gorm" +) + +func getTxAndContext() (tx *gorm.DB, ctx context.Context) { + tx = global.DB.Begin() + ctx = context.WithValue(context.Background(), constant.DB, tx) + return +} diff --git a/agent/app/service/host.go b/agent/app/service/host.go new file mode 100644 index 000000000..85a93fba3 --- /dev/null +++ b/agent/app/service/host.go @@ -0,0 +1,325 @@ +package service + +import ( + "encoding/base64" + "fmt" + + "github.com/1Panel-dev/1Panel/agent/app/dto" + "github.com/1Panel-dev/1Panel/agent/app/model" + "github.com/1Panel-dev/1Panel/agent/constant" + "github.com/1Panel-dev/1Panel/agent/utils/encrypt" + "github.com/1Panel-dev/1Panel/agent/utils/ssh" + "github.com/jinzhu/copier" + "github.com/pkg/errors" +) + +type HostService struct{} + +type IHostService interface { + TestLocalConn(id uint) bool + TestByInfo(req dto.HostConnTest) bool + GetHostInfo(id uint) (*model.Host, error) + SearchForTree(search dto.SearchForTree) ([]dto.HostTree, error) + SearchWithPage(search dto.SearchHostWithPage) (int64, interface{}, error) + Create(hostDto dto.HostOperate) (*dto.HostInfo, error) + Update(id uint, upMap map[string]interface{}) error + Delete(id []uint) error + + EncryptHost(itemVal string) (string, error) +} + +func NewIHostService() IHostService { + return &HostService{} +} + +func (u *HostService) TestByInfo(req dto.HostConnTest) bool { + if req.AuthMode == "password" && len(req.Password) != 0 { + password, err := base64.StdEncoding.DecodeString(req.Password) + if err != nil { + return false + } + req.Password = string(password) + } + if req.AuthMode == "key" && len(req.PrivateKey) != 0 { + privateKey, err := base64.StdEncoding.DecodeString(req.PrivateKey) + if err != nil { + return false + } + req.PrivateKey = string(privateKey) + } + if len(req.Password) == 0 && len(req.PrivateKey) == 0 { + host, err := hostRepo.Get(hostRepo.WithByAddr(req.Addr)) + if err != nil { + return false + } + req.Password = host.Password + req.AuthMode = host.AuthMode + req.PrivateKey = host.PrivateKey + req.PassPhrase = host.PassPhrase + } + + var connInfo ssh.ConnInfo + _ = copier.Copy(&connInfo, &req) + connInfo.PrivateKey = []byte(req.PrivateKey) + if len(req.PassPhrase) != 0 { + connInfo.PassPhrase = []byte(req.PassPhrase) + } + client, err := connInfo.NewClient() + if err != nil { + return false + } + defer client.Close() + return true +} + +func (u *HostService) TestLocalConn(id uint) bool { + var ( + host model.Host + err error + ) + if id == 0 { + host, err = hostRepo.Get(hostRepo.WithByAddr("127.0.0.1")) + if err != nil { + return false + } + } else { + host, err = hostRepo.Get(commonRepo.WithByID(id)) + if err != nil { + return false + } + } + var connInfo ssh.ConnInfo + if err := copier.Copy(&connInfo, &host); err != nil { + return false + } + if len(host.Password) != 0 { + host.Password, err = encrypt.StringDecrypt(host.Password) + if err != nil { + return false + } + connInfo.Password = host.Password + } + if len(host.PrivateKey) != 0 { + host.PrivateKey, err = encrypt.StringDecrypt(host.PrivateKey) + if err != nil { + return false + } + connInfo.PrivateKey = []byte(host.PrivateKey) + } + if len(host.PassPhrase) != 0 { + host.PassPhrase, err = encrypt.StringDecrypt(host.PassPhrase) + if err != nil { + return false + } + connInfo.PassPhrase = []byte(host.PassPhrase) + } + client, err := connInfo.NewClient() + if err != nil { + return false + } + defer client.Close() + + return true +} + +func (u *HostService) GetHostInfo(id uint) (*model.Host, error) { + host, err := hostRepo.Get(commonRepo.WithByID(id)) + if err != nil { + return nil, constant.ErrRecordNotFound + } + if len(host.Password) != 0 { + host.Password, err = encrypt.StringDecrypt(host.Password) + if err != nil { + return nil, err + } + } + if len(host.PrivateKey) != 0 { + host.PrivateKey, err = encrypt.StringDecrypt(host.PrivateKey) + if err != nil { + return nil, err + } + } + + if len(host.PassPhrase) != 0 { + host.PassPhrase, err = encrypt.StringDecrypt(host.PassPhrase) + if err != nil { + return nil, err + } + } + return &host, err +} + +func (u *HostService) SearchWithPage(search dto.SearchHostWithPage) (int64, interface{}, error) { + total, hosts, err := hostRepo.Page(search.Page, search.PageSize, hostRepo.WithByInfo(search.Info), commonRepo.WithByGroupID(search.GroupID)) + if err != nil { + return 0, nil, err + } + var dtoHosts []dto.HostInfo + for _, host := range hosts { + var item dto.HostInfo + if err := copier.Copy(&item, &host); err != nil { + return 0, nil, errors.WithMessage(constant.ErrStructTransform, err.Error()) + } + group, _ := groupRepo.Get(commonRepo.WithByID(host.GroupID)) + item.GroupBelong = group.Name + if !item.RememberPassword { + item.Password = "" + item.PrivateKey = "" + item.PassPhrase = "" + } else { + if len(host.Password) != 0 { + item.Password, err = encrypt.StringDecrypt(host.Password) + if err != nil { + return 0, nil, err + } + } + if len(host.PrivateKey) != 0 { + item.PrivateKey, err = encrypt.StringDecrypt(host.PrivateKey) + if err != nil { + return 0, nil, err + } + } + if len(host.PassPhrase) != 0 { + item.PassPhrase, err = encrypt.StringDecrypt(host.PassPhrase) + if err != nil { + return 0, nil, err + } + } + } + dtoHosts = append(dtoHosts, item) + } + return total, dtoHosts, err +} + +func (u *HostService) SearchForTree(search dto.SearchForTree) ([]dto.HostTree, error) { + hosts, err := hostRepo.GetList(hostRepo.WithByInfo(search.Info)) + if err != nil { + return nil, err + } + groups, err := groupRepo.GetList(commonRepo.WithByType("host")) + if err != nil { + return nil, err + } + var datas []dto.HostTree + for _, group := range groups { + var data dto.HostTree + data.ID = group.ID + 10000 + data.Label = group.Name + for _, host := range hosts { + label := fmt.Sprintf("%s@%s:%d", host.User, host.Addr, host.Port) + if len(host.Name) != 0 { + label = fmt.Sprintf("%s - %s@%s:%d", host.Name, host.User, host.Addr, host.Port) + } + if host.GroupID == group.ID { + data.Children = append(data.Children, dto.TreeChild{ID: host.ID, Label: label}) + } + } + if len(data.Children) != 0 { + datas = append(datas, data) + } + } + return datas, err +} + +func (u *HostService) Create(req dto.HostOperate) (*dto.HostInfo, error) { + var err error + if len(req.Password) != 0 && req.AuthMode == "password" { + req.Password, err = u.EncryptHost(req.Password) + if err != nil { + return nil, err + } + req.PrivateKey = "" + req.PassPhrase = "" + } + if len(req.PrivateKey) != 0 && req.AuthMode == "key" { + req.PrivateKey, err = u.EncryptHost(req.PrivateKey) + if err != nil { + return nil, err + } + if len(req.PassPhrase) != 0 { + req.PassPhrase, err = encrypt.StringEncrypt(req.PassPhrase) + if err != nil { + return nil, err + } + } + req.Password = "" + } + var host model.Host + if err := copier.Copy(&host, &req); err != nil { + return nil, errors.WithMessage(constant.ErrStructTransform, err.Error()) + } + if req.GroupID == 0 { + group, err := groupRepo.Get(groupRepo.WithByHostDefault()) + if err != nil { + return nil, errors.New("get default group failed") + } + host.GroupID = group.ID + req.GroupID = group.ID + } + var sameHostID uint + if req.Addr == "127.0.0.1" { + hostSame, _ := hostRepo.Get(hostRepo.WithByAddr(req.Addr)) + sameHostID = hostSame.ID + } else { + hostSame, _ := hostRepo.Get(hostRepo.WithByAddr(req.Addr), hostRepo.WithByUser(req.User), hostRepo.WithByPort(req.Port)) + sameHostID = hostSame.ID + } + if sameHostID != 0 { + host.ID = sameHostID + upMap := make(map[string]interface{}) + upMap["name"] = req.Name + upMap["group_id"] = req.GroupID + upMap["addr"] = req.Addr + upMap["port"] = req.Port + upMap["user"] = req.User + upMap["auth_mode"] = req.AuthMode + upMap["password"] = req.Password + upMap["private_key"] = req.PrivateKey + upMap["pass_phrase"] = req.PassPhrase + upMap["remember_password"] = req.RememberPassword + upMap["description"] = req.Description + if err := hostRepo.Update(sameHostID, upMap); err != nil { + return nil, err + } + var hostinfo dto.HostInfo + if err := copier.Copy(&hostinfo, &host); err != nil { + return nil, errors.WithMessage(constant.ErrStructTransform, err.Error()) + } + return &hostinfo, nil + } + + if err := hostRepo.Create(&host); err != nil { + return nil, err + } + var hostinfo dto.HostInfo + if err := copier.Copy(&hostinfo, &host); err != nil { + return nil, errors.WithMessage(constant.ErrStructTransform, err.Error()) + } + return &hostinfo, nil +} + +func (u *HostService) Delete(ids []uint) error { + hosts, _ := hostRepo.GetList(commonRepo.WithIdsIn(ids)) + for _, host := range hosts { + if host.ID == 0 { + return constant.ErrRecordNotFound + } + if host.Addr == "127.0.0.1" { + return errors.New("the local connection information cannot be deleted!") + } + } + return hostRepo.Delete(commonRepo.WithIdsIn(ids)) +} + +func (u *HostService) Update(id uint, upMap map[string]interface{}) error { + return hostRepo.Update(id, upMap) +} + +func (u *HostService) EncryptHost(itemVal string) (string, error) { + privateKey, err := base64.StdEncoding.DecodeString(itemVal) + if err != nil { + return "", err + } + keyItem, err := encrypt.StringEncrypt(string(privateKey)) + return keyItem, err +} diff --git a/agent/app/service/host_tool.go b/agent/app/service/host_tool.go new file mode 100644 index 000000000..6252171f3 --- /dev/null +++ b/agent/app/service/host_tool.go @@ -0,0 +1,566 @@ +package service + +import ( + "bytes" + "fmt" + "os/exec" + "os/user" + "path" + "strconv" + "strings" + + "github.com/1Panel-dev/1Panel/agent/app/dto/request" + "github.com/1Panel-dev/1Panel/agent/app/dto/response" + "github.com/1Panel-dev/1Panel/agent/buserr" + "github.com/1Panel-dev/1Panel/agent/constant" + "github.com/1Panel-dev/1Panel/agent/global" + "github.com/1Panel-dev/1Panel/agent/utils/cmd" + "github.com/1Panel-dev/1Panel/agent/utils/files" + "github.com/1Panel-dev/1Panel/agent/utils/ini_conf" + "github.com/1Panel-dev/1Panel/agent/utils/systemctl" + "github.com/pkg/errors" + "gopkg.in/ini.v1" +) + +type HostToolService struct{} + +type IHostToolService interface { + GetToolStatus(req request.HostToolReq) (*response.HostToolRes, error) + CreateToolConfig(req request.HostToolCreate) error + OperateTool(req request.HostToolReq) error + OperateToolConfig(req request.HostToolConfig) (*response.HostToolConfig, error) + GetToolLog(req request.HostToolLogReq) (string, error) + OperateSupervisorProcess(req request.SupervisorProcessConfig) error + GetSupervisorProcessConfig() ([]response.SupervisorProcessConfig, error) + OperateSupervisorProcessFile(req request.SupervisorProcessFileReq) (string, error) +} + +func NewIHostToolService() IHostToolService { + return &HostToolService{} +} + +func (h *HostToolService) GetToolStatus(req request.HostToolReq) (*response.HostToolRes, error) { + res := &response.HostToolRes{} + res.Type = req.Type + switch req.Type { + case constant.Supervisord: + supervisorConfig := &response.Supervisor{} + if !cmd.Which(constant.Supervisord) { + supervisorConfig.IsExist = false + res.Config = supervisorConfig + return res, nil + } + supervisorConfig.IsExist = true + serviceExist, _ := systemctl.IsExist(constant.Supervisord) + if !serviceExist { + serviceExist, _ = systemctl.IsExist(constant.Supervisor) + if !serviceExist { + supervisorConfig.IsExist = false + res.Config = supervisorConfig + return res, nil + } else { + supervisorConfig.ServiceName = constant.Supervisor + } + } else { + supervisorConfig.ServiceName = constant.Supervisord + } + + serviceNameSet, _ := settingRepo.Get(settingRepo.WithByKey(constant.SupervisorServiceName)) + if serviceNameSet.ID != 0 || serviceNameSet.Value != "" { + supervisorConfig.ServiceName = serviceNameSet.Value + } + + versionRes, _ := cmd.Exec("supervisord -v") + supervisorConfig.Version = strings.TrimSuffix(versionRes, "\n") + _, ctlRrr := exec.LookPath("supervisorctl") + supervisorConfig.CtlExist = ctlRrr == nil + + active, _ := systemctl.IsActive(supervisorConfig.ServiceName) + if active { + supervisorConfig.Status = "running" + } else { + supervisorConfig.Status = "stopped" + } + + pathSet, _ := settingRepo.Get(settingRepo.WithByKey(constant.SupervisorConfigPath)) + if pathSet.ID != 0 || pathSet.Value != "" { + supervisorConfig.ConfigPath = pathSet.Value + res.Config = supervisorConfig + return res, nil + } else { + supervisorConfig.Init = true + } + + servicePath := "/usr/lib/systemd/system/supervisor.service" + fileOp := files.NewFileOp() + if !fileOp.Stat(servicePath) { + servicePath = "/usr/lib/systemd/system/supervisord.service" + } + if fileOp.Stat(servicePath) { + startCmd, _ := ini_conf.GetIniValue(servicePath, "Service", "ExecStart") + if startCmd != "" { + args := strings.Fields(startCmd) + cIndex := -1 + for i, arg := range args { + if arg == "-c" { + cIndex = i + break + } + } + if cIndex != -1 && cIndex+1 < len(args) { + supervisorConfig.ConfigPath = args[cIndex+1] + } + } + } + if supervisorConfig.ConfigPath == "" { + configPath := "/etc/supervisord.conf" + if !fileOp.Stat(configPath) { + configPath = "/etc/supervisor/supervisord.conf" + if fileOp.Stat(configPath) { + supervisorConfig.ConfigPath = configPath + } + } + } + + res.Config = supervisorConfig + } + return res, nil +} + +func (h *HostToolService) CreateToolConfig(req request.HostToolCreate) error { + switch req.Type { + case constant.Supervisord: + fileOp := files.NewFileOp() + if !fileOp.Stat(req.ConfigPath) { + return buserr.New("ErrConfigNotFound") + } + cfg, err := ini.Load(req.ConfigPath) + if err != nil { + return err + } + service, err := cfg.GetSection("include") + if err != nil { + return err + } + targetKey, err := service.GetKey("files") + if err != nil { + return err + } + if targetKey != nil { + _, err = service.NewKey(";files", targetKey.Value()) + if err != nil { + return err + } + } + supervisorDir := path.Join(global.CONF.System.BaseDir, "1panel", "tools", "supervisord") + includeDir := path.Join(supervisorDir, "supervisor.d") + if !fileOp.Stat(includeDir) { + if err = fileOp.CreateDir(includeDir, 0755); err != nil { + return err + } + } + logDir := path.Join(supervisorDir, "log") + if !fileOp.Stat(logDir) { + if err = fileOp.CreateDir(logDir, 0755); err != nil { + return err + } + } + includePath := path.Join(includeDir, "*.ini") + targetKey.SetValue(includePath) + if err = cfg.SaveTo(req.ConfigPath); err != nil { + return err + } + + serviceNameSet, _ := settingRepo.Get(settingRepo.WithByKey(constant.SupervisorServiceName)) + if serviceNameSet.ID != 0 { + if err = settingRepo.Update(constant.SupervisorServiceName, req.ServiceName); err != nil { + return err + } + } else { + if err = settingRepo.Create(constant.SupervisorServiceName, req.ServiceName); err != nil { + return err + } + } + + configPathSet, _ := settingRepo.Get(settingRepo.WithByKey(constant.SupervisorConfigPath)) + if configPathSet.ID != 0 { + if err = settingRepo.Update(constant.SupervisorConfigPath, req.ConfigPath); err != nil { + return err + } + } else { + if err = settingRepo.Create(constant.SupervisorConfigPath, req.ConfigPath); err != nil { + return err + } + } + if err = systemctl.Restart(req.ServiceName); err != nil { + global.LOG.Errorf("[init] restart %s failed err %s", req.ServiceName, err.Error()) + return err + } + } + return nil +} + +func (h *HostToolService) OperateTool(req request.HostToolReq) error { + serviceName := req.Type + if req.Type == constant.Supervisord { + serviceNameSet, _ := settingRepo.Get(settingRepo.WithByKey(constant.SupervisorServiceName)) + if serviceNameSet.ID != 0 || serviceNameSet.Value != "" { + serviceName = serviceNameSet.Value + } + } + return systemctl.Operate(req.Operate, serviceName) +} + +func (h *HostToolService) OperateToolConfig(req request.HostToolConfig) (*response.HostToolConfig, error) { + fileOp := files.NewFileOp() + res := &response.HostToolConfig{} + configPath := "" + serviceName := "supervisord" + switch req.Type { + case constant.Supervisord: + pathSet, _ := settingRepo.Get(settingRepo.WithByKey(constant.SupervisorConfigPath)) + if pathSet.ID != 0 || pathSet.Value != "" { + configPath = pathSet.Value + } + serviceNameSet, _ := settingRepo.Get(settingRepo.WithByKey(constant.SupervisorServiceName)) + if serviceNameSet.ID != 0 || serviceNameSet.Value != "" { + serviceName = serviceNameSet.Value + } + } + switch req.Operate { + case "get": + content, err := fileOp.GetContent(configPath) + if err != nil { + return nil, err + } + res.Content = string(content) + case "set": + file, err := fileOp.OpenFile(configPath) + if err != nil { + return nil, err + } + oldContent, err := fileOp.GetContent(configPath) + if err != nil { + return nil, err + } + fileInfo, err := file.Stat() + if err != nil { + return nil, err + } + if err = fileOp.WriteFile(configPath, strings.NewReader(req.Content), fileInfo.Mode()); err != nil { + return nil, err + } + if err = systemctl.Restart(serviceName); err != nil { + _ = fileOp.WriteFile(configPath, bytes.NewReader(oldContent), fileInfo.Mode()) + return nil, err + } + } + + return res, nil +} + +func (h *HostToolService) GetToolLog(req request.HostToolLogReq) (string, error) { + fileOp := files.NewFileOp() + logfilePath := "" + switch req.Type { + case constant.Supervisord: + configPath := "/etc/supervisord.conf" + pathSet, _ := settingRepo.Get(settingRepo.WithByKey(constant.SupervisorConfigPath)) + if pathSet.ID != 0 || pathSet.Value != "" { + configPath = pathSet.Value + } + logfilePath, _ = ini_conf.GetIniValue(configPath, "supervisord", "logfile") + } + oldContent, err := fileOp.GetContent(logfilePath) + if err != nil { + return "", err + } + return string(oldContent), nil +} + +func (h *HostToolService) OperateSupervisorProcess(req request.SupervisorProcessConfig) error { + var ( + supervisordDir = path.Join(global.CONF.System.BaseDir, "1panel", "tools", "supervisord") + logDir = path.Join(supervisordDir, "log") + includeDir = path.Join(supervisordDir, "supervisor.d") + outLog = path.Join(logDir, fmt.Sprintf("%s.out.log", req.Name)) + errLog = path.Join(logDir, fmt.Sprintf("%s.err.log", req.Name)) + iniPath = path.Join(includeDir, fmt.Sprintf("%s.ini", req.Name)) + fileOp = files.NewFileOp() + ) + if req.Operate == "update" || req.Operate == "create" { + if !fileOp.Stat(req.Dir) { + return buserr.New("ErrConfigDirNotFound") + } + _, err := user.Lookup(req.User) + if err != nil { + return buserr.WithMap("ErrUserFindErr", map[string]interface{}{"name": req.User, "err": err.Error()}, err) + } + } + + switch req.Operate { + case "create": + if fileOp.Stat(iniPath) { + return buserr.New("ErrConfigAlreadyExist") + } + configFile := ini.Empty() + section, err := configFile.NewSection(fmt.Sprintf("program:%s", req.Name)) + if err != nil { + return err + } + _, _ = section.NewKey("command", strings.TrimSpace(req.Command)) + _, _ = section.NewKey("directory", req.Dir) + _, _ = section.NewKey("autorestart", "true") + _, _ = section.NewKey("startsecs", "3") + _, _ = section.NewKey("stdout_logfile", outLog) + _, _ = section.NewKey("stderr_logfile", errLog) + _, _ = section.NewKey("stdout_logfile_maxbytes", "2MB") + _, _ = section.NewKey("stderr_logfile_maxbytes", "2MB") + _, _ = section.NewKey("user", req.User) + _, _ = section.NewKey("priority", "999") + _, _ = section.NewKey("numprocs", req.Numprocs) + _, _ = section.NewKey("process_name", "%(program_name)s_%(process_num)02d") + + if err = configFile.SaveTo(iniPath); err != nil { + return err + } + if err := operateSupervisorCtl("reread", "", ""); err != nil { + return err + } + return operateSupervisorCtl("update", "", "") + case "update": + configFile, err := ini.Load(iniPath) + if err != nil { + return err + } + section, err := configFile.GetSection(fmt.Sprintf("program:%s", req.Name)) + if err != nil { + return err + } + + commandKey := section.Key("command") + commandKey.SetValue(strings.TrimSpace(req.Command)) + directoryKey := section.Key("directory") + directoryKey.SetValue(req.Dir) + userKey := section.Key("user") + userKey.SetValue(req.User) + numprocsKey := section.Key("numprocs") + numprocsKey.SetValue(req.Numprocs) + + if err = configFile.SaveTo(iniPath); err != nil { + return err + } + if err := operateSupervisorCtl("reread", "", ""); err != nil { + return err + } + return operateSupervisorCtl("update", "", "") + case "restart": + return operateSupervisorCtl("restart", req.Name, "") + case "start": + return operateSupervisorCtl("start", req.Name, "") + case "stop": + return operateSupervisorCtl("stop", req.Name, "") + case "delete": + _ = operateSupervisorCtl("remove", "", req.Name) + _ = files.NewFileOp().DeleteFile(iniPath) + _ = files.NewFileOp().DeleteFile(outLog) + _ = files.NewFileOp().DeleteFile(errLog) + if err := operateSupervisorCtl("reread", "", ""); err != nil { + return err + } + return operateSupervisorCtl("update", "", "") + } + + return nil +} + +func (h *HostToolService) GetSupervisorProcessConfig() ([]response.SupervisorProcessConfig, error) { + var ( + result []response.SupervisorProcessConfig + ) + configDir := path.Join(global.CONF.System.BaseDir, "1panel", "tools", "supervisord", "supervisor.d") + fileList, _ := NewIFileService().GetFileList(request.FileOption{FileOption: files.FileOption{Path: configDir, Expand: true, Page: 1, PageSize: 100}}) + if len(fileList.Items) == 0 { + return result, nil + } + for _, configFile := range fileList.Items { + f, err := ini.Load(configFile.Path) + if err != nil { + global.LOG.Errorf("get %s file err %s", configFile.Name, err.Error()) + continue + } + if strings.HasSuffix(configFile.Name, ".ini") { + config := response.SupervisorProcessConfig{} + name := strings.TrimSuffix(configFile.Name, ".ini") + config.Name = name + section, err := f.GetSection(fmt.Sprintf("program:%s", name)) + if err != nil { + global.LOG.Errorf("get %s file section err %s", configFile.Name, err.Error()) + continue + } + if command, _ := section.GetKey("command"); command != nil { + config.Command = command.Value() + } + if directory, _ := section.GetKey("directory"); directory != nil { + config.Dir = directory.Value() + } + if user, _ := section.GetKey("user"); user != nil { + config.User = user.Value() + } + if numprocs, _ := section.GetKey("numprocs"); numprocs != nil { + config.Numprocs = numprocs.Value() + } + _ = getProcessStatus(&config) + result = append(result, config) + } + } + return result, nil +} + +func (h *HostToolService) OperateSupervisorProcessFile(req request.SupervisorProcessFileReq) (string, error) { + var ( + fileOp = files.NewFileOp() + group = fmt.Sprintf("program:%s", req.Name) + configPath = path.Join(global.CONF.System.BaseDir, "1panel", "tools", "supervisord", "supervisor.d", fmt.Sprintf("%s.ini", req.Name)) + ) + switch req.File { + case "err.log": + logPath, err := ini_conf.GetIniValue(configPath, group, "stderr_logfile") + if err != nil { + return "", err + } + switch req.Operate { + case "get": + content, err := fileOp.GetContent(logPath) + if err != nil { + return "", err + } + return string(content), nil + case "clear": + if err = fileOp.WriteFile(logPath, strings.NewReader(""), 0755); err != nil { + return "", err + } + } + + case "out.log": + logPath, err := ini_conf.GetIniValue(configPath, group, "stdout_logfile") + if err != nil { + return "", err + } + switch req.Operate { + case "get": + content, err := fileOp.GetContent(logPath) + if err != nil { + return "", err + } + return string(content), nil + case "clear": + if err = fileOp.WriteFile(logPath, strings.NewReader(""), 0755); err != nil { + return "", err + } + } + + case "config": + switch req.Operate { + case "get": + content, err := fileOp.GetContent(configPath) + if err != nil { + return "", err + } + return string(content), nil + case "update": + if req.Content == "" { + return "", buserr.New("ErrConfigIsNull") + } + if err := fileOp.WriteFile(configPath, strings.NewReader(req.Content), 0755); err != nil { + return "", err + } + return "", operateSupervisorCtl("update", "", req.Name) + } + + } + return "", nil +} + +func operateSupervisorCtl(operate, name, group string) error { + processNames := []string{operate} + if name != "" { + includeDir := path.Join(global.CONF.System.BaseDir, "1panel", "tools", "supervisord", "supervisor.d") + f, err := ini.Load(path.Join(includeDir, fmt.Sprintf("%s.ini", name))) + if err != nil { + return err + } + section, err := f.GetSection(fmt.Sprintf("program:%s", name)) + if err != nil { + return err + } + numprocsNum := "" + if numprocs, _ := section.GetKey("numprocs"); numprocs != nil { + numprocsNum = numprocs.Value() + } + if numprocsNum == "" { + return buserr.New("ErrConfigParse") + } + processNames = append(processNames, getProcessName(name, numprocsNum)...) + } + if group != "" { + processNames = append(processNames, group) + } + + output, err := exec.Command("supervisorctl", processNames...).Output() + if err != nil { + if output != nil { + return errors.New(string(output)) + } + return err + } + return nil +} + +func getProcessName(name, numprocs string) []string { + var ( + processNames []string + ) + num, err := strconv.Atoi(numprocs) + if err != nil { + return processNames + } + if num == 1 { + processNames = append(processNames, fmt.Sprintf("%s:%s_00", name, name)) + } else { + for i := 0; i < num; i++ { + processName := fmt.Sprintf("%s:%s_0%s", name, name, strconv.Itoa(i)) + if i >= 10 { + processName = fmt.Sprintf("%s:%s_%s", name, name, strconv.Itoa(i)) + } + processNames = append(processNames, processName) + } + } + return processNames +} + +func getProcessStatus(config *response.SupervisorProcessConfig) error { + var ( + processNames = []string{"status"} + ) + processNames = append(processNames, getProcessName(config.Name, config.Numprocs)...) + output, _ := exec.Command("supervisorctl", processNames...).Output() + lines := strings.Split(string(output), "\n") + for _, line := range lines { + fields := strings.Fields(line) + if len(fields) >= 5 { + status := response.ProcessStatus{ + Name: fields[0], + Status: fields[1], + } + if fields[1] == "RUNNING" { + status.PID = strings.TrimSuffix(fields[3], ",") + status.Uptime = fields[5] + } else { + status.Msg = strings.Join(fields[2:], " ") + } + config.Status = append(config.Status, status) + } + } + return nil +} diff --git a/agent/app/service/image.go b/agent/app/service/image.go new file mode 100644 index 000000000..affecd1d6 --- /dev/null +++ b/agent/app/service/image.go @@ -0,0 +1,517 @@ +package service + +import ( + "bufio" + "context" + "encoding/base64" + "encoding/json" + "errors" + "fmt" + "io" + "os" + "path" + "strings" + "time" + + "github.com/docker/docker/api/types/container" + "github.com/docker/docker/api/types/image" + + "github.com/1Panel-dev/1Panel/agent/app/dto" + "github.com/1Panel-dev/1Panel/agent/buserr" + "github.com/1Panel-dev/1Panel/agent/constant" + "github.com/1Panel-dev/1Panel/agent/global" + "github.com/1Panel-dev/1Panel/agent/utils/docker" + "github.com/docker/docker/api/types" + "github.com/docker/docker/api/types/registry" + "github.com/docker/docker/pkg/archive" + "github.com/docker/docker/pkg/homedir" +) + +type ImageService struct{} + +type IImageService interface { + Page(req dto.SearchWithPage) (int64, interface{}, error) + List() ([]dto.Options, error) + ListAll() ([]dto.ImageInfo, error) + ImageBuild(req dto.ImageBuild) (string, error) + ImagePull(req dto.ImagePull) (string, error) + ImageLoad(req dto.ImageLoad) error + ImageSave(req dto.ImageSave) error + ImagePush(req dto.ImagePush) (string, error) + ImageRemove(req dto.BatchDelete) error + ImageTag(req dto.ImageTag) error +} + +func NewIImageService() IImageService { + return &ImageService{} +} +func (u *ImageService) Page(req dto.SearchWithPage) (int64, interface{}, error) { + var ( + list []image.Summary + records []dto.ImageInfo + backDatas []dto.ImageInfo + ) + client, err := docker.NewDockerClient() + if err != nil { + return 0, nil, err + } + defer client.Close() + list, err = client.ImageList(context.Background(), image.ListOptions{}) + if err != nil { + return 0, nil, err + } + containers, _ := client.ContainerList(context.Background(), container.ListOptions{All: true}) + if len(req.Info) != 0 { + length, count := len(list), 0 + for count < length { + hasTag := false + for _, tag := range list[count].RepoTags { + if strings.Contains(tag, req.Info) { + hasTag = true + break + } + } + if !hasTag { + list = append(list[:count], list[(count+1):]...) + length-- + } else { + count++ + } + } + } + + for _, image := range list { + size := formatFileSize(image.Size) + records = append(records, dto.ImageInfo{ + ID: image.ID, + Tags: image.RepoTags, + IsUsed: checkUsed(image.ID, containers), + CreatedAt: time.Unix(image.Created, 0), + Size: size, + }) + } + total, start, end := len(records), (req.Page-1)*req.PageSize, req.Page*req.PageSize + if start > total { + backDatas = make([]dto.ImageInfo, 0) + } else { + if end >= total { + end = total + } + backDatas = records[start:end] + } + + return int64(total), backDatas, nil +} + +func (u *ImageService) ListAll() ([]dto.ImageInfo, error) { + var records []dto.ImageInfo + client, err := docker.NewDockerClient() + if err != nil { + return nil, err + } + defer client.Close() + list, err := client.ImageList(context.Background(), image.ListOptions{}) + if err != nil { + return nil, err + } + containers, _ := client.ContainerList(context.Background(), container.ListOptions{All: true}) + for _, image := range list { + size := formatFileSize(image.Size) + records = append(records, dto.ImageInfo{ + ID: image.ID, + Tags: image.RepoTags, + IsUsed: checkUsed(image.ID, containers), + CreatedAt: time.Unix(image.Created, 0), + Size: size, + }) + } + return records, nil +} + +func (u *ImageService) List() ([]dto.Options, error) { + var ( + list []image.Summary + backDatas []dto.Options + ) + client, err := docker.NewDockerClient() + if err != nil { + return nil, err + } + defer client.Close() + list, err = client.ImageList(context.Background(), image.ListOptions{}) + if err != nil { + return nil, err + } + for _, image := range list { + for _, tag := range image.RepoTags { + backDatas = append(backDatas, dto.Options{ + Option: tag, + }) + } + } + return backDatas, nil +} + +func (u *ImageService) ImageBuild(req dto.ImageBuild) (string, error) { + client, err := docker.NewDockerClient() + if err != nil { + return "", err + } + defer client.Close() + fileName := "Dockerfile" + if req.From == "edit" { + dir := fmt.Sprintf("%s/docker/build/%s", constant.DataDir, strings.ReplaceAll(req.Name, ":", "_")) + if _, err := os.Stat(dir); err != nil && os.IsNotExist(err) { + if err = os.MkdirAll(dir, os.ModePerm); err != nil { + return "", err + } + } + + pathItem := fmt.Sprintf("%s/Dockerfile", dir) + file, err := os.OpenFile(pathItem, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0666) + if err != nil { + return "", err + } + defer file.Close() + write := bufio.NewWriter(file) + _, _ = write.WriteString(string(req.Dockerfile)) + write.Flush() + req.Dockerfile = dir + } else { + fileName = path.Base(req.Dockerfile) + req.Dockerfile = path.Dir(req.Dockerfile) + } + tar, err := archive.TarWithOptions(req.Dockerfile+"/", &archive.TarOptions{}) + if err != nil { + return "", err + } + + opts := types.ImageBuildOptions{ + Dockerfile: fileName, + Tags: []string{req.Name}, + Remove: true, + Labels: stringsToMap(req.Tags), + } + + dockerLogDir := path.Join(global.CONF.System.TmpDir, "docker_logs") + if _, err := os.Stat(dockerLogDir); err != nil && os.IsNotExist(err) { + if err = os.MkdirAll(dockerLogDir, os.ModePerm); err != nil { + return "", err + } + } + logItem := fmt.Sprintf("%s/image_build_%s_%s.log", dockerLogDir, strings.ReplaceAll(req.Name, ":", "_"), time.Now().Format(constant.DateTimeSlimLayout)) + file, err := os.OpenFile(logItem, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0666) + if err != nil { + return "", err + } + go func() { + defer file.Close() + defer tar.Close() + res, err := client.ImageBuild(context.Background(), tar, opts) + if err != nil { + global.LOG.Errorf("build image %s failed, err: %v", req.Name, err) + _, _ = file.WriteString("image build failed!") + return + } + defer res.Body.Close() + body, err := io.ReadAll(res.Body) + if err != nil { + global.LOG.Errorf("build image %s failed, err: %v", req.Name, err) + _, _ = file.WriteString(fmt.Sprintf("build image %s failed, err: %v", req.Name, err)) + _, _ = file.WriteString("image build failed!") + return + } + + if strings.Contains(string(body), "errorDetail") || strings.Contains(string(body), "error:") { + global.LOG.Errorf("build image %s failed", req.Name) + _, _ = file.Write(body) + _, _ = file.WriteString("image build failed!") + return + } + global.LOG.Infof("build image %s successful!", req.Name) + _, _ = file.Write(body) + _, _ = file.WriteString("image build successful!") + }() + + return path.Base(logItem), nil +} + +func (u *ImageService) ImagePull(req dto.ImagePull) (string, error) { + client, err := docker.NewDockerClient() + if err != nil { + return "", err + } + defer client.Close() + dockerLogDir := path.Join(global.CONF.System.TmpDir, "docker_logs") + if _, err := os.Stat(dockerLogDir); err != nil && os.IsNotExist(err) { + if err = os.MkdirAll(dockerLogDir, os.ModePerm); err != nil { + return "", err + } + } + imageItemName := strings.ReplaceAll(path.Base(req.ImageName), ":", "_") + logItem := fmt.Sprintf("%s/image_pull_%s_%s.log", dockerLogDir, imageItemName, time.Now().Format(constant.DateTimeSlimLayout)) + file, err := os.OpenFile(logItem, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0666) + if err != nil { + return "", err + } + options := image.PullOptions{} + if req.RepoID == 0 { + hasAuth, authStr := loadAuthInfo(req.ImageName) + if hasAuth { + options.RegistryAuth = authStr + } + go func() { + defer file.Close() + out, err := client.ImagePull(context.TODO(), req.ImageName, options) + if err != nil { + global.LOG.Errorf("image %s pull failed, err: %v", req.ImageName, err) + return + } + defer out.Close() + global.LOG.Infof("pull image %s successful!", req.ImageName) + _, _ = io.Copy(file, out) + }() + return path.Base(logItem), nil + } + repo, err := imageRepoRepo.Get(commonRepo.WithByID(req.RepoID)) + if err != nil { + return "", err + } + if repo.Auth { + authConfig := registry.AuthConfig{ + Username: repo.Username, + Password: repo.Password, + } + encodedJSON, err := json.Marshal(authConfig) + if err != nil { + return "", err + } + authStr := base64.URLEncoding.EncodeToString(encodedJSON) + options.RegistryAuth = authStr + } + image := repo.DownloadUrl + "/" + req.ImageName + go func() { + defer file.Close() + out, err := client.ImagePull(context.TODO(), image, options) + if err != nil { + _, _ = file.WriteString("image pull failed!") + global.LOG.Errorf("image %s pull failed, err: %v", image, err) + return + } + defer out.Close() + global.LOG.Infof("pull image %s successful!", req.ImageName) + _, _ = io.Copy(file, out) + _, _ = file.WriteString("image pull successful!") + }() + return path.Base(logItem), nil +} + +func (u *ImageService) ImageLoad(req dto.ImageLoad) error { + file, err := os.Open(req.Path) + if err != nil { + return err + } + defer file.Close() + client, err := docker.NewDockerClient() + if err != nil { + return err + } + defer client.Close() + res, err := client.ImageLoad(context.TODO(), file, true) + if err != nil { + return err + } + defer res.Body.Close() + content, err := io.ReadAll(res.Body) + if err != nil { + return err + } + if strings.Contains(string(content), "Error") { + return errors.New(string(content)) + } + return nil +} + +func (u *ImageService) ImageSave(req dto.ImageSave) error { + client, err := docker.NewDockerClient() + if err != nil { + return err + } + defer client.Close() + + out, err := client.ImageSave(context.TODO(), []string{req.TagName}) + if err != nil { + return err + } + defer out.Close() + file, err := os.OpenFile(fmt.Sprintf("%s/%s.tar", req.Path, req.Name), os.O_WRONLY|os.O_CREATE|os.O_EXCL, 0666) + if err != nil { + return err + } + defer file.Close() + if _, err = io.Copy(file, out); err != nil { + return err + } + return nil +} + +func (u *ImageService) ImageTag(req dto.ImageTag) error { + client, err := docker.NewDockerClient() + if err != nil { + return err + } + defer client.Close() + + if err := client.ImageTag(context.TODO(), req.SourceID, req.TargetName); err != nil { + return err + } + return nil +} + +func (u *ImageService) ImagePush(req dto.ImagePush) (string, error) { + client, err := docker.NewDockerClient() + if err != nil { + return "", err + } + defer client.Close() + repo, err := imageRepoRepo.Get(commonRepo.WithByID(req.RepoID)) + if err != nil { + return "", err + } + options := image.PushOptions{All: true} + authConfig := registry.AuthConfig{ + Username: repo.Username, + Password: repo.Password, + } + encodedJSON, err := json.Marshal(authConfig) + if err != nil { + return "", err + } + authStr := base64.URLEncoding.EncodeToString(encodedJSON) + options.RegistryAuth = authStr + newName := fmt.Sprintf("%s/%s", repo.DownloadUrl, req.Name) + if newName != req.TagName { + if err := client.ImageTag(context.TODO(), req.TagName, newName); err != nil { + return "", err + } + } + + dockerLogDir := global.CONF.System.TmpDir + "/docker_logs" + if _, err := os.Stat(dockerLogDir); err != nil && os.IsNotExist(err) { + if err = os.MkdirAll(dockerLogDir, os.ModePerm); err != nil { + return "", err + } + } + imageItemName := strings.ReplaceAll(path.Base(req.Name), ":", "_") + logItem := fmt.Sprintf("%s/image_push_%s_%s.log", dockerLogDir, imageItemName, time.Now().Format(constant.DateTimeSlimLayout)) + file, err := os.OpenFile(logItem, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0666) + if err != nil { + return "", err + } + go func() { + defer file.Close() + out, err := client.ImagePush(context.TODO(), newName, options) + if err != nil { + global.LOG.Errorf("image %s push failed, err: %v", req.TagName, err) + _, _ = file.WriteString("image push failed!") + return + } + defer out.Close() + global.LOG.Infof("push image %s successful!", req.Name) + _, _ = io.Copy(file, out) + _, _ = file.WriteString("image push successful!") + }() + + return path.Base(logItem), nil +} + +func (u *ImageService) ImageRemove(req dto.BatchDelete) error { + client, err := docker.NewDockerClient() + if err != nil { + return err + } + defer client.Close() + for _, id := range req.Names { + if _, err := client.ImageRemove(context.TODO(), id, image.RemoveOptions{Force: req.Force, PruneChildren: true}); err != nil { + if strings.Contains(err.Error(), "image is being used") || strings.Contains(err.Error(), "is using") { + if strings.Contains(id, "sha256:") { + return buserr.New(constant.ErrObjectInUsed) + } + return buserr.WithDetail(constant.ErrInUsed, id, nil) + } + return err + } + } + return nil +} + +func formatFileSize(fileSize int64) (size string) { + if fileSize < 1024 { + return fmt.Sprintf("%.2fB", float64(fileSize)/float64(1)) + } else if fileSize < (1024 * 1024) { + return fmt.Sprintf("%.2fKB", float64(fileSize)/float64(1024)) + } else if fileSize < (1024 * 1024 * 1024) { + return fmt.Sprintf("%.2fMB", float64(fileSize)/float64(1024*1024)) + } else if fileSize < (1024 * 1024 * 1024 * 1024) { + return fmt.Sprintf("%.2fGB", float64(fileSize)/float64(1024*1024*1024)) + } else if fileSize < (1024 * 1024 * 1024 * 1024 * 1024) { + return fmt.Sprintf("%.2fTB", float64(fileSize)/float64(1024*1024*1024*1024)) + } else { + return fmt.Sprintf("%.2fEB", float64(fileSize)/float64(1024*1024*1024*1024*1024)) + } +} + +func checkUsed(imageID string, containers []types.Container) bool { + for _, container := range containers { + if container.ImageID == imageID { + return true + } + } + return false +} + +func loadAuthInfo(image string) (bool, string) { + if !strings.Contains(image, "/") { + return false, "" + } + homeDir := homedir.Get() + confPath := path.Join(homeDir, ".docker/config.json") + configFileBytes, err := os.ReadFile(confPath) + if err != nil { + return false, "" + } + var config dockerConfig + if err = json.Unmarshal(configFileBytes, &config); err != nil { + return false, "" + } + var ( + user string + passwd string + ) + imagePrefix := strings.Split(image, "/")[0] + if val, ok := config.Auths[imagePrefix]; ok { + itemByte, _ := base64.StdEncoding.DecodeString(val.Auth) + itemStr := string(itemByte) + if strings.Contains(itemStr, ":") { + user = strings.Split(itemStr, ":")[0] + passwd = strings.Split(itemStr, ":")[1] + } + } + authConfig := registry.AuthConfig{ + Username: user, + Password: passwd, + } + encodedJSON, err := json.Marshal(authConfig) + if err != nil { + return false, "" + } + authStr := base64.URLEncoding.EncodeToString(encodedJSON) + return true, authStr +} + +type dockerConfig struct { + Auths map[string]authConfig `json:"auths"` +} +type authConfig struct { + Auth string `json:"auth"` +} diff --git a/agent/app/service/image_repo.go b/agent/app/service/image_repo.go new file mode 100644 index 000000000..535e393b8 --- /dev/null +++ b/agent/app/service/image_repo.go @@ -0,0 +1,243 @@ +package service + +import ( + "context" + "encoding/json" + "fmt" + "os" + "strings" + "time" + + "github.com/1Panel-dev/1Panel/agent/app/dto" + "github.com/1Panel-dev/1Panel/agent/buserr" + "github.com/1Panel-dev/1Panel/agent/constant" + "github.com/1Panel-dev/1Panel/agent/global" + "github.com/1Panel-dev/1Panel/agent/utils/cmd" + "github.com/1Panel-dev/1Panel/agent/utils/common" + "github.com/jinzhu/copier" + "github.com/pkg/errors" +) + +type ImageRepoService struct{} + +type IImageRepoService interface { + Page(search dto.SearchWithPage) (int64, interface{}, error) + List() ([]dto.ImageRepoOption, error) + Login(req dto.OperateByID) error + Create(req dto.ImageRepoCreate) error + Update(req dto.ImageRepoUpdate) error + BatchDelete(req dto.ImageRepoDelete) error +} + +func NewIImageRepoService() IImageRepoService { + return &ImageRepoService{} +} + +func (u *ImageRepoService) Page(req dto.SearchWithPage) (int64, interface{}, error) { + total, ops, err := imageRepoRepo.Page(req.Page, req.PageSize, commonRepo.WithLikeName(req.Info), commonRepo.WithOrderBy("created_at desc")) + var dtoOps []dto.ImageRepoInfo + for _, op := range ops { + var item dto.ImageRepoInfo + if err := copier.Copy(&item, &op); err != nil { + return 0, nil, errors.WithMessage(constant.ErrStructTransform, err.Error()) + } + dtoOps = append(dtoOps, item) + } + return total, dtoOps, err +} + +func (u *ImageRepoService) Login(req dto.OperateByID) error { + repo, err := imageRepoRepo.Get(commonRepo.WithByID(req.ID)) + if err != nil { + return err + } + if repo.Auth { + if err := u.CheckConn(repo.DownloadUrl, repo.Username, repo.Password); err != nil { + _ = imageRepoRepo.Update(repo.ID, map[string]interface{}{"status": constant.StatusFailed, "message": err.Error()}) + return err + } + } + _ = imageRepoRepo.Update(repo.ID, map[string]interface{}{"status": constant.StatusSuccess}) + return nil +} + +func (u *ImageRepoService) List() ([]dto.ImageRepoOption, error) { + ops, err := imageRepoRepo.List(commonRepo.WithOrderBy("created_at desc")) + var dtoOps []dto.ImageRepoOption + for _, op := range ops { + if op.Status == constant.StatusSuccess { + var item dto.ImageRepoOption + if err := copier.Copy(&item, &op); err != nil { + return nil, errors.WithMessage(constant.ErrStructTransform, err.Error()) + } + dtoOps = append(dtoOps, item) + } + } + return dtoOps, err +} + +func (u *ImageRepoService) Create(req dto.ImageRepoCreate) error { + if cmd.CheckIllegal(req.Username, req.Password, req.DownloadUrl) { + return buserr.New(constant.ErrCmdIllegal) + } + imageRepo, _ := imageRepoRepo.Get(commonRepo.WithByName(req.Name)) + if imageRepo.ID != 0 { + return constant.ErrRecordExist + } + if req.Protocol == "http" { + _ = u.handleRegistries(req.DownloadUrl, "", "create") + stdout, err := cmd.Exec("systemctl restart docker") + if err != nil { + return errors.New(string(stdout)) + } + ticker := time.NewTicker(3 * time.Second) + defer ticker.Stop() + ctx, cancel := context.WithTimeout(context.Background(), time.Second*20) + if err := func() error { + for range ticker.C { + select { + case <-ctx.Done(): + cancel() + return errors.New("the docker service cannot be restarted") + default: + stdout, err := cmd.Exec("systemctl is-active docker") + if string(stdout) == "active\n" && err == nil { + global.LOG.Info("docker restart with new conf successful!") + return nil + } + } + } + return nil + }(); err != nil { + return err + } + } + + if err := copier.Copy(&imageRepo, &req); err != nil { + return errors.WithMessage(constant.ErrStructTransform, err.Error()) + } + + imageRepo.Status = constant.StatusSuccess + if req.Auth { + if err := u.CheckConn(req.DownloadUrl, req.Username, req.Password); err != nil { + imageRepo.Status = constant.StatusFailed + imageRepo.Message = err.Error() + } + } + if err := imageRepoRepo.Create(&imageRepo); err != nil { + return err + } + + return nil +} + +func (u *ImageRepoService) BatchDelete(req dto.ImageRepoDelete) error { + for _, id := range req.Ids { + if id == 1 { + return errors.New("The default value cannot be edit !") + } + } + if err := imageRepoRepo.Delete(commonRepo.WithIdsIn(req.Ids)); err != nil { + return err + } + return nil +} + +func (u *ImageRepoService) Update(req dto.ImageRepoUpdate) error { + if req.ID == 1 { + return errors.New("The default value cannot be deleted !") + } + if cmd.CheckIllegal(req.Username, req.Password, req.DownloadUrl) { + return buserr.New(constant.ErrCmdIllegal) + } + repo, err := imageRepoRepo.Get(commonRepo.WithByID(req.ID)) + if err != nil { + return err + } + if repo.DownloadUrl != req.DownloadUrl || (!repo.Auth && req.Auth) { + _ = u.handleRegistries(req.DownloadUrl, repo.DownloadUrl, "update") + if repo.Auth { + _, _ = cmd.ExecWithCheck("docker", "logout", repo.DownloadUrl) + } + stdout, err := cmd.Exec("systemctl restart docker") + if err != nil { + return errors.New(string(stdout)) + } + } + + upMap := make(map[string]interface{}) + upMap["download_url"] = req.DownloadUrl + upMap["protocol"] = req.Protocol + upMap["username"] = req.Username + upMap["password"] = req.Password + upMap["auth"] = req.Auth + + upMap["status"] = constant.StatusSuccess + upMap["message"] = "" + if req.Auth { + if err := u.CheckConn(req.DownloadUrl, req.Username, req.Password); err != nil { + upMap["status"] = constant.StatusFailed + upMap["message"] = err.Error() + } + } + return imageRepoRepo.Update(req.ID, upMap) +} + +func (u *ImageRepoService) CheckConn(host, user, password string) error { + stdout, err := cmd.ExecWithCheck("docker", "login", "-u", user, "-p", password, host) + if err != nil { + return fmt.Errorf("stdout: %s, stderr: %v", stdout, err) + } + if strings.Contains(string(stdout), "Login Succeeded") { + return nil + } + return errors.New(string(stdout)) +} + +func (u *ImageRepoService) handleRegistries(newHost, delHost, handle string) error { + err := createIfNotExistDaemonJsonFile() + if err != nil { + return err + } + daemonMap := make(map[string]interface{}) + file, err := os.ReadFile(constant.DaemonJsonPath) + if err != nil { + return err + } + if err := json.Unmarshal(file, &daemonMap); err != nil { + return err + } + + iRegistries := daemonMap["insecure-registries"] + registries, _ := iRegistries.([]interface{}) + switch handle { + case "create": + registries = common.RemoveRepeatElement(append(registries, newHost)) + case "update": + for i, regi := range registries { + if regi == delHost { + registries = append(registries[:i], registries[i+1:]...) + } + } + registries = common.RemoveRepeatElement(append(registries, newHost)) + case "delete": + for i, regi := range registries { + if regi == delHost { + registries = append(registries[:i], registries[i+1:]...) + } + } + } + if len(registries) == 0 { + delete(daemonMap, "insecure-registries") + } else { + daemonMap["insecure-registries"] = registries + } + newJson, err := json.MarshalIndent(daemonMap, "", "\t") + if err != nil { + return err + } + if err := os.WriteFile(constant.DaemonJsonPath, newJson, 0640); err != nil { + return err + } + return nil +} diff --git a/agent/app/service/logs.go b/agent/app/service/logs.go new file mode 100644 index 000000000..ce9276469 --- /dev/null +++ b/agent/app/service/logs.go @@ -0,0 +1,81 @@ +package service + +import ( + "fmt" + "os" + "path" + "path/filepath" + "sort" + "strings" + "time" + + "github.com/1Panel-dev/1Panel/agent/buserr" + "github.com/1Panel-dev/1Panel/agent/global" +) + +type LogService struct{} + +type ILogService interface { + ListSystemLogFile() ([]string, error) + LoadSystemLog(name string) (string, error) +} + +func NewILogService() ILogService { + return &LogService{} +} + +func (u *LogService) ListSystemLogFile() ([]string, error) { + logDir := path.Join(global.CONF.System.BaseDir, "1panel/log") + var files []string + if err := filepath.Walk(logDir, func(pathItem string, info os.FileInfo, err error) error { + if err != nil { + return err + } + if !info.IsDir() && strings.HasPrefix(info.Name(), "1Panel") { + if info.Name() == "1Panel.log" { + files = append(files, time.Now().Format("2006-01-02")) + return nil + } + itemFileName := strings.TrimPrefix(info.Name(), "1Panel-") + itemFileName = strings.TrimSuffix(itemFileName, ".gz") + itemFileName = strings.TrimSuffix(itemFileName, ".log") + files = append(files, itemFileName) + return nil + } + return nil + }); err != nil { + return nil, err + } + + if len(files) < 2 { + return files, nil + } + sort.Slice(files, func(i, j int) bool { + return files[i] > files[j] + }) + + return files, nil +} + +func (u *LogService) LoadSystemLog(name string) (string, error) { + if name == time.Now().Format("2006-01-02") { + name = "1Panel.log" + } else { + name = "1Panel-" + name + ".log" + } + filePath := path.Join(global.CONF.System.DataDir, "log", name) + if _, err := os.Stat(filePath); err != nil { + fileGzPath := path.Join(global.CONF.System.DataDir, "log", name+".gz") + if _, err := os.Stat(fileGzPath); err != nil { + return "", buserr.New("ErrHttpReqNotFound") + } + if err := handleGunzip(fileGzPath); err != nil { + return "", fmt.Errorf("handle ungzip file %s failed, err: %v", fileGzPath, err) + } + } + content, err := os.ReadFile(filePath) + if err != nil { + return "", err + } + return string(content), nil +} diff --git a/agent/app/service/monitor.go b/agent/app/service/monitor.go new file mode 100644 index 000000000..5ea0f47e6 --- /dev/null +++ b/agent/app/service/monitor.go @@ -0,0 +1,216 @@ +package service + +import ( + "context" + "fmt" + "strconv" + "time" + + "github.com/1Panel-dev/1Panel/agent/app/model" + "github.com/1Panel-dev/1Panel/agent/global" + "github.com/robfig/cron/v3" + "github.com/shirou/gopsutil/v3/cpu" + "github.com/shirou/gopsutil/v3/disk" + "github.com/shirou/gopsutil/v3/load" + "github.com/shirou/gopsutil/v3/mem" + "github.com/shirou/gopsutil/v3/net" +) + +type MonitorService struct { + DiskIO chan ([]disk.IOCountersStat) + NetIO chan ([]net.IOCountersStat) +} + +var monitorCancel context.CancelFunc + +type IMonitorService interface { + Run() + + saveIODataToDB(ctx context.Context, interval float64) + saveNetDataToDB(ctx context.Context, interval float64) +} + +func NewIMonitorService() IMonitorService { + return &MonitorService{ + DiskIO: make(chan []disk.IOCountersStat, 2), + NetIO: make(chan []net.IOCountersStat, 2), + } +} + +func (m *MonitorService) Run() { + var itemModel model.MonitorBase + totalPercent, _ := cpu.Percent(3*time.Second, false) + if len(totalPercent) == 1 { + itemModel.Cpu = totalPercent[0] + } + cpuCount, _ := cpu.Counts(false) + + loadInfo, _ := load.Avg() + itemModel.CpuLoad1 = loadInfo.Load1 + itemModel.CpuLoad5 = loadInfo.Load5 + itemModel.CpuLoad15 = loadInfo.Load15 + itemModel.LoadUsage = loadInfo.Load1 / (float64(cpuCount*2) * 0.75) * 100 + + memoryInfo, _ := mem.VirtualMemory() + itemModel.Memory = memoryInfo.UsedPercent + + if err := settingRepo.CreateMonitorBase(itemModel); err != nil { + global.LOG.Errorf("Insert basic monitoring data failed, err: %v", err) + } + + m.loadDiskIO() + m.loadNetIO() + + MonitorStoreDays, err := settingRepo.Get(settingRepo.WithByKey("MonitorStoreDays")) + if err != nil { + return + } + storeDays, _ := strconv.Atoi(MonitorStoreDays.Value) + timeForDelete := time.Now().AddDate(0, 0, -storeDays) + _ = settingRepo.DelMonitorBase(timeForDelete) + _ = settingRepo.DelMonitorIO(timeForDelete) + _ = settingRepo.DelMonitorNet(timeForDelete) +} + +func (m *MonitorService) loadDiskIO() { + ioStat, _ := disk.IOCounters() + var diskIOList []disk.IOCountersStat + for _, io := range ioStat { + diskIOList = append(diskIOList, io) + } + m.DiskIO <- diskIOList +} + +func (m *MonitorService) loadNetIO() { + netStat, _ := net.IOCounters(true) + netStatAll, _ := net.IOCounters(false) + var netList []net.IOCountersStat + netList = append(netList, netStat...) + netList = append(netList, netStatAll...) + m.NetIO <- netList +} + +func (m *MonitorService) saveIODataToDB(ctx context.Context, interval float64) { + defer close(m.DiskIO) + for { + select { + case <-ctx.Done(): + return + case ioStat := <-m.DiskIO: + select { + case <-ctx.Done(): + return + case ioStat2 := <-m.DiskIO: + var ioList []model.MonitorIO + for _, io2 := range ioStat2 { + for _, io1 := range ioStat { + if io2.Name == io1.Name { + var itemIO model.MonitorIO + itemIO.Name = io1.Name + if io2.ReadBytes != 0 && io1.ReadBytes != 0 && io2.ReadBytes > io1.ReadBytes { + itemIO.Read = uint64(float64(io2.ReadBytes-io1.ReadBytes) / interval / 60) + } + if io2.WriteBytes != 0 && io1.WriteBytes != 0 && io2.WriteBytes > io1.WriteBytes { + itemIO.Write = uint64(float64(io2.WriteBytes-io1.WriteBytes) / interval / 60) + } + + if io2.ReadCount != 0 && io1.ReadCount != 0 && io2.ReadCount > io1.ReadCount { + itemIO.Count = uint64(float64(io2.ReadCount-io1.ReadCount) / interval / 60) + } + writeCount := uint64(0) + if io2.WriteCount != 0 && io1.WriteCount != 0 && io2.WriteCount > io1.WriteCount { + writeCount = uint64(float64(io2.WriteCount-io1.WriteCount) / interval * 60) + } + if writeCount > itemIO.Count { + itemIO.Count = writeCount + } + + if io2.ReadTime != 0 && io1.ReadTime != 0 && io2.ReadTime > io1.ReadTime { + itemIO.Time = uint64(float64(io2.ReadTime-io1.ReadTime) / interval / 60) + } + writeTime := uint64(0) + if io2.WriteTime != 0 && io1.WriteTime != 0 && io2.WriteTime > io1.WriteTime { + writeTime = uint64(float64(io2.WriteTime-io1.WriteTime) / interval / 60) + } + if writeTime > itemIO.Time { + itemIO.Time = writeTime + } + ioList = append(ioList, itemIO) + break + } + } + } + if err := settingRepo.BatchCreateMonitorIO(ioList); err != nil { + global.LOG.Errorf("Insert io monitoring data failed, err: %v", err) + } + m.DiskIO <- ioStat2 + } + } + } +} + +func (m *MonitorService) saveNetDataToDB(ctx context.Context, interval float64) { + defer close(m.NetIO) + for { + select { + case <-ctx.Done(): + return + case netStat := <-m.NetIO: + select { + case <-ctx.Done(): + return + case netStat2 := <-m.NetIO: + var netList []model.MonitorNetwork + for _, net2 := range netStat2 { + for _, net1 := range netStat { + if net2.Name == net1.Name { + var itemNet model.MonitorNetwork + itemNet.Name = net1.Name + + if net2.BytesSent != 0 && net1.BytesSent != 0 && net2.BytesSent > net1.BytesSent { + itemNet.Up = float64(net2.BytesSent-net1.BytesSent) / 1024 / interval / 60 + } + if net2.BytesRecv != 0 && net1.BytesRecv != 0 && net2.BytesRecv > net1.BytesRecv { + itemNet.Down = float64(net2.BytesRecv-net1.BytesRecv) / 1024 / interval / 60 + } + netList = append(netList, itemNet) + break + } + } + } + + if err := settingRepo.BatchCreateMonitorNet(netList); err != nil { + global.LOG.Errorf("Insert network monitoring data failed, err: %v", err) + } + m.NetIO <- netStat2 + } + } + } +} + +func StartMonitor(removeBefore bool, interval string) error { + if removeBefore { + monitorCancel() + global.Cron.Remove(cron.EntryID(global.MonitorCronID)) + } + intervalItem, err := strconv.Atoi(interval) + if err != nil { + return err + } + + service := NewIMonitorService() + ctx, cancel := context.WithCancel(context.Background()) + monitorCancel = cancel + monitorID, err := global.Cron.AddJob(fmt.Sprintf("@every %sm", interval), service) + if err != nil { + return err + } + + service.Run() + + go service.saveIODataToDB(ctx, float64(intervalItem)) + go service.saveNetDataToDB(ctx, float64(intervalItem)) + + global.MonitorCronID = monitorID + return nil +} diff --git a/agent/app/service/nginx.go b/agent/app/service/nginx.go new file mode 100644 index 000000000..82950aade --- /dev/null +++ b/agent/app/service/nginx.go @@ -0,0 +1,146 @@ +package service + +import ( + "fmt" + "io" + "net/http" + "os" + "path" + "strings" + "time" + + "github.com/1Panel-dev/1Panel/agent/utils/compose" + + "github.com/1Panel-dev/1Panel/agent/app/dto/request" + "github.com/1Panel-dev/1Panel/agent/app/dto/response" + + "github.com/1Panel-dev/1Panel/agent/app/dto" + "github.com/1Panel-dev/1Panel/agent/constant" + "github.com/1Panel-dev/1Panel/agent/utils/files" +) + +type NginxService struct { +} + +type INginxService interface { + GetNginxConfig() (*response.NginxFile, error) + GetConfigByScope(req request.NginxScopeReq) ([]response.NginxParam, error) + UpdateConfigByScope(req request.NginxConfigUpdate) error + GetStatus() (response.NginxStatus, error) + UpdateConfigFile(req request.NginxConfigFileUpdate) error + ClearProxyCache() error +} + +func NewINginxService() INginxService { + return &NginxService{} +} + +func (n NginxService) GetNginxConfig() (*response.NginxFile, error) { + nginxInstall, err := getAppInstallByKey(constant.AppOpenresty) + if err != nil { + return nil, err + } + configPath := path.Join(constant.AppInstallDir, constant.AppOpenresty, nginxInstall.Name, "conf", "nginx.conf") + byteContent, err := files.NewFileOp().GetContent(configPath) + if err != nil { + return nil, err + } + return &response.NginxFile{Content: string(byteContent)}, nil +} + +func (n NginxService) GetConfigByScope(req request.NginxScopeReq) ([]response.NginxParam, error) { + keys, ok := dto.ScopeKeyMap[req.Scope] + if !ok || len(keys) == 0 { + return nil, nil + } + return getNginxParamsByKeys(constant.NginxScopeHttp, keys, nil) +} + +func (n NginxService) UpdateConfigByScope(req request.NginxConfigUpdate) error { + keys, ok := dto.ScopeKeyMap[req.Scope] + if !ok || len(keys) == 0 { + return nil + } + return updateNginxConfig(constant.NginxScopeHttp, getNginxParams(req.Params, keys), nil) +} + +func (n NginxService) GetStatus() (response.NginxStatus, error) { + httpPort, _, err := getAppInstallPort(constant.AppOpenresty) + if err != nil { + return response.NginxStatus{}, err + } + url := "http://127.0.0.1/nginx_status" + if httpPort != 80 { + url = fmt.Sprintf("http://127.0.0.1:%v/nginx_status", httpPort) + } + res, err := http.Get(url) + if err != nil { + return response.NginxStatus{}, err + } + defer res.Body.Close() + content, err := io.ReadAll(res.Body) + if err != nil { + return response.NginxStatus{}, err + } + var status response.NginxStatus + resArray := strings.Split(string(content), " ") + status.Active = resArray[2] + status.Accepts = resArray[7] + status.Handled = resArray[8] + status.Requests = resArray[9] + status.Reading = resArray[11] + status.Writing = resArray[13] + status.Waiting = resArray[15] + return status, nil +} + +func (n NginxService) UpdateConfigFile(req request.NginxConfigFileUpdate) error { + fileOp := files.NewFileOp() + nginxInstall, err := getAppInstallByKey(constant.AppOpenresty) + filePath := path.Join(constant.AppInstallDir, constant.AppOpenresty, nginxInstall.Name, "conf", "nginx.conf") + if err != nil { + return err + } + if req.Backup { + backupPath := path.Join(path.Dir(filePath), "bak") + if !fileOp.Stat(backupPath) { + if err := fileOp.CreateDir(backupPath, 0755); err != nil { + return err + } + } + newFile := path.Join(backupPath, "nginx.bak"+"-"+time.Now().Format("2006-01-02-15-04-05")) + if err := fileOp.Copy(filePath, backupPath); err != nil { + return err + } + if err := fileOp.Rename(path.Join(backupPath, "nginx.conf"), newFile); err != nil { + return err + } + } + oldContent, err := os.ReadFile(filePath) + if err != nil { + return err + } + if err = fileOp.WriteFile(filePath, strings.NewReader(req.Content), 0644); err != nil { + return err + } + return nginxCheckAndReload(string(oldContent), filePath, nginxInstall.ContainerName) +} + +func (n NginxService) ClearProxyCache() error { + nginxInstall, err := getAppInstallByKey(constant.AppOpenresty) + if err != nil { + return err + } + cacheDir := path.Join(nginxInstall.GetPath(), "www/common/proxy/proxy_cache_dir") + fileOp := files.NewFileOp() + if fileOp.Stat(cacheDir) { + if err = fileOp.CleanDir(cacheDir); err != nil { + return err + } + _, err = compose.Restart(nginxInstall.GetComposePath()) + if err != nil { + return err + } + } + return nil +} diff --git a/agent/app/service/nginx_utils.go b/agent/app/service/nginx_utils.go new file mode 100644 index 000000000..4d571bb10 --- /dev/null +++ b/agent/app/service/nginx_utils.go @@ -0,0 +1,232 @@ +package service + +import ( + "errors" + "fmt" + "os" + "path" + "strings" + "time" + + "github.com/1Panel-dev/1Panel/agent/app/dto" + "github.com/1Panel-dev/1Panel/agent/app/dto/response" + "github.com/1Panel-dev/1Panel/agent/app/model" + "github.com/1Panel-dev/1Panel/agent/cmd/server/nginx_conf" + "github.com/1Panel-dev/1Panel/agent/constant" + "github.com/1Panel-dev/1Panel/agent/utils/cmd" + "github.com/1Panel-dev/1Panel/agent/utils/files" + "github.com/1Panel-dev/1Panel/agent/utils/nginx" + "github.com/1Panel-dev/1Panel/agent/utils/nginx/components" + "github.com/1Panel-dev/1Panel/agent/utils/nginx/parser" +) + +func getNginxFull(website *model.Website) (dto.NginxFull, error) { + var nginxFull dto.NginxFull + nginxInstall, err := getAppInstallByKey("openresty") + if err != nil { + return nginxFull, err + } + nginxFull.Install = nginxInstall + nginxFull.Dir = path.Join(constant.AppInstallDir, constant.AppOpenresty, nginxInstall.Name) + nginxFull.ConfigDir = path.Join(nginxFull.Dir, "conf") + nginxFull.ConfigFile = "nginx.conf" + nginxFull.SiteDir = path.Join(nginxFull.Dir, "www") + + var nginxConfig dto.NginxConfig + nginxConfig.FilePath = path.Join(nginxFull.Dir, "conf", "nginx.conf") + content, err := os.ReadFile(path.Join(nginxFull.ConfigDir, nginxFull.ConfigFile)) + if err != nil { + return nginxFull, err + } + config, err := parser.NewStringParser(string(content)).Parse() + if err != nil { + return dto.NginxFull{}, err + } + config.FilePath = nginxConfig.FilePath + nginxConfig.OldContent = string(content) + nginxConfig.Config = config + + nginxFull.RootConfig = nginxConfig + + if website != nil { + nginxFull.Website = *website + var siteNginxConfig dto.NginxConfig + nginxFileName := website.Alias + ".conf" + siteConfigPath := path.Join(constant.AppInstallDir, constant.AppOpenresty, nginxInstall.Name, "conf", "conf.d", nginxFileName) + siteNginxConfig.FilePath = siteConfigPath + siteNginxContent, err := os.ReadFile(siteConfigPath) + if err != nil { + return nginxFull, err + } + siteConfig, err := parser.NewStringParser(string(siteNginxContent)).Parse() + if err != nil { + return dto.NginxFull{}, err + } + siteConfig.FilePath = siteConfigPath + siteNginxConfig.Config = siteConfig + siteNginxConfig.OldContent = string(siteNginxContent) + nginxFull.SiteConfig = siteNginxConfig + } + + return nginxFull, nil +} + +func getNginxParamsByKeys(scope string, keys []string, website *model.Website) ([]response.NginxParam, error) { + nginxFull, err := getNginxFull(website) + if err != nil { + return nil, err + } + var res []response.NginxParam + var block components.IBlock + if scope == constant.NginxScopeHttp { + block = nginxFull.RootConfig.Config.FindHttp() + } else { + block = nginxFull.SiteConfig.Config.FindServers()[0] + } + for _, key := range keys { + dirs := block.FindDirectives(key) + for _, dir := range dirs { + nginxParam := response.NginxParam{ + Name: dir.GetName(), + Params: dir.GetParameters(), + } + res = append(res, nginxParam) + } + if len(dirs) == 0 { + nginxParam := response.NginxParam{ + Name: key, + Params: []string{}, + } + res = append(res, nginxParam) + } + } + return res, nil +} + +func updateNginxConfig(scope string, params []dto.NginxParam, website *model.Website) error { + nginxFull, err := getNginxFull(website) + if err != nil { + return err + } + var block components.IBlock + var config dto.NginxConfig + if scope == constant.NginxScopeHttp { + config = nginxFull.RootConfig + block = nginxFull.RootConfig.Config.FindHttp() + } else if scope == constant.NginxScopeServer { + config = nginxFull.SiteConfig + block = nginxFull.SiteConfig.Config.FindServers()[0] + } else { + config = nginxFull.SiteConfig + block = config.Config.Block + } + + for _, p := range params { + if p.UpdateScope == constant.NginxScopeOut { + config.Config.UpdateDirective(p.Name, p.Params) + } else { + block.UpdateDirective(p.Name, p.Params) + } + } + if err := nginx.WriteConfig(config.Config, nginx.IndentedStyle); err != nil { + return err + } + return nginxCheckAndReload(config.OldContent, config.FilePath, nginxFull.Install.ContainerName) +} + +func deleteNginxConfig(scope string, params []dto.NginxParam, website *model.Website) error { + nginxFull, err := getNginxFull(website) + if err != nil { + return err + } + var block components.IBlock + var config dto.NginxConfig + if scope == constant.NginxScopeHttp { + config = nginxFull.RootConfig + block = nginxFull.RootConfig.Config.FindHttp() + } else if scope == constant.NginxScopeServer { + config = nginxFull.SiteConfig + block = nginxFull.SiteConfig.Config.FindServers()[0] + } else { + config = nginxFull.SiteConfig + block = config.Config.Block + } + + for _, param := range params { + block.RemoveDirective(param.Name, param.Params) + } + + if err := nginx.WriteConfig(config.Config, nginx.IndentedStyle); err != nil { + return err + } + return nginxCheckAndReload(config.OldContent, config.FilePath, nginxFull.Install.ContainerName) +} + +func getNginxParamsFromStaticFile(scope dto.NginxKey, newParams []dto.NginxParam) []dto.NginxParam { + var ( + newConfig = &components.Config{} + err error + ) + + updateScope := "in" + switch scope { + case dto.SSL: + newConfig, err = parser.NewStringParser(string(nginx_conf.SSL)).Parse() + case dto.CACHE: + newConfig, err = parser.NewStringParser(string(nginx_conf.Cache)).Parse() + case dto.ProxyCache: + newConfig, err = parser.NewStringParser(string(nginx_conf.ProxyCache)).Parse() + } + if err != nil { + return nil + } + for _, dir := range newConfig.GetDirectives() { + addParam := dto.NginxParam{ + Name: dir.GetName(), + Params: dir.GetParameters(), + UpdateScope: updateScope, + } + isExist := false + for _, newParam := range newParams { + if newParam.Name == dir.GetName() { + if components.IsRepeatKey(newParam.Name) { + if len(newParam.Params) > 0 && newParam.Params[0] == dir.GetParameters()[0] { + isExist = true + } + } else { + isExist = true + } + } + } + if !isExist { + newParams = append(newParams, addParam) + } + } + return newParams +} + +func opNginx(containerName, operate string) error { + nginxCmd := fmt.Sprintf("docker exec -i %s %s", containerName, "nginx -s reload") + if operate == constant.NginxCheck { + nginxCmd = fmt.Sprintf("docker exec -i %s %s", containerName, "nginx -t") + } + if out, err := cmd.ExecWithTimeOut(nginxCmd, 20*time.Second); err != nil { + if out != "" { + return errors.New(out) + } + return err + } + return nil +} + +func nginxCheckAndReload(oldContent string, filePath string, containerName string) error { + if err := opNginx(containerName, constant.NginxCheck); err != nil { + _ = files.NewFileOp().WriteFile(filePath, strings.NewReader(oldContent), 0644) + return err + } + if err := opNginx(containerName, constant.NginxReload); err != nil { + _ = files.NewFileOp().WriteFile(filePath, strings.NewReader(oldContent), 0644) + return err + } + return nil +} diff --git a/agent/app/service/php_extensions.go b/agent/app/service/php_extensions.go new file mode 100644 index 000000000..a7686da5e --- /dev/null +++ b/agent/app/service/php_extensions.go @@ -0,0 +1,86 @@ +package service + +import ( + "github.com/1Panel-dev/1Panel/agent/app/dto/request" + "github.com/1Panel-dev/1Panel/agent/app/dto/response" + "github.com/1Panel-dev/1Panel/agent/app/model" + "github.com/1Panel-dev/1Panel/agent/buserr" + "github.com/1Panel-dev/1Panel/agent/constant" +) + +type PHPExtensionsService struct { +} + +type IPHPExtensionsService interface { + Page(req request.PHPExtensionsSearch) (int64, []response.PHPExtensionsDTO, error) + List() ([]response.PHPExtensionsDTO, error) + Create(req request.PHPExtensionsCreate) error + Update(req request.PHPExtensionsUpdate) error + Delete(req request.PHPExtensionsDelete) error +} + +func NewIPHPExtensionsService() IPHPExtensionsService { + return &PHPExtensionsService{} +} + +func (p PHPExtensionsService) Page(req request.PHPExtensionsSearch) (int64, []response.PHPExtensionsDTO, error) { + var ( + total int64 + extensions []model.PHPExtensions + err error + result []response.PHPExtensionsDTO + ) + total, extensions, err = phpExtensionsRepo.Page(req.Page, req.PageSize) + if err != nil { + return 0, nil, err + } + for _, extension := range extensions { + result = append(result, response.PHPExtensionsDTO{ + PHPExtensions: extension, + }) + } + return total, result, nil +} + +func (p PHPExtensionsService) List() ([]response.PHPExtensionsDTO, error) { + var ( + extensions []model.PHPExtensions + err error + result []response.PHPExtensionsDTO + ) + extensions, err = phpExtensionsRepo.List() + if err != nil { + return nil, err + } + for _, extension := range extensions { + result = append(result, response.PHPExtensionsDTO{ + PHPExtensions: extension, + }) + } + return result, nil +} + +func (p PHPExtensionsService) Create(req request.PHPExtensionsCreate) error { + exist, _ := phpExtensionsRepo.GetFirst(commonRepo.WithByName(req.Name)) + if exist.ID > 0 { + return buserr.New(constant.ErrNameIsExist) + } + extension := model.PHPExtensions{ + Name: req.Name, + Extensions: req.Extensions, + } + return phpExtensionsRepo.Create(&extension) +} + +func (p PHPExtensionsService) Update(req request.PHPExtensionsUpdate) error { + exist, err := phpExtensionsRepo.GetFirst(commonRepo.WithByID(req.ID)) + if err != nil { + return err + } + exist.Extensions = req.Extensions + return phpExtensionsRepo.Save(&exist) +} + +func (p PHPExtensionsService) Delete(req request.PHPExtensionsDelete) error { + return phpExtensionsRepo.DeleteBy(commonRepo.WithByID(req.ID)) +} diff --git a/agent/app/service/process.go b/agent/app/service/process.go new file mode 100644 index 000000000..b16de17de --- /dev/null +++ b/agent/app/service/process.go @@ -0,0 +1,27 @@ +package service + +import ( + "github.com/1Panel-dev/1Panel/agent/app/dto/request" + "github.com/shirou/gopsutil/v3/process" +) + +type ProcessService struct{} + +type IProcessService interface { + StopProcess(req request.ProcessReq) error +} + +func NewIProcessService() IProcessService { + return &ProcessService{} +} + +func (p *ProcessService) StopProcess(req request.ProcessReq) error { + proc, err := process.NewProcess(req.PID) + if err != nil { + return err + } + if err := proc.Kill(); err != nil { + return err + } + return nil +} diff --git a/agent/app/service/recycle_bin.go b/agent/app/service/recycle_bin.go new file mode 100644 index 000000000..7e8fc9e32 --- /dev/null +++ b/agent/app/service/recycle_bin.go @@ -0,0 +1,212 @@ +package service + +import ( + "fmt" + "math" + "os" + "path" + "regexp" + "strconv" + "strings" + "time" + + "github.com/1Panel-dev/1Panel/agent/app/dto" + "github.com/1Panel-dev/1Panel/agent/app/dto/request" + "github.com/1Panel-dev/1Panel/agent/app/dto/response" + "github.com/1Panel-dev/1Panel/agent/buserr" + "github.com/1Panel-dev/1Panel/agent/constant" + "github.com/1Panel-dev/1Panel/agent/utils/files" + "github.com/shirou/gopsutil/v3/disk" +) + +type RecycleBinService struct { +} + +type IRecycleBinService interface { + Page(search dto.PageInfo) (int64, []response.RecycleBinDTO, error) + Create(create request.RecycleBinCreate) error + Reduce(reduce request.RecycleBinReduce) error + Clear() error +} + +func NewIRecycleBinService() IRecycleBinService { + return &RecycleBinService{} +} + +func (r RecycleBinService) Page(search dto.PageInfo) (int64, []response.RecycleBinDTO, error) { + var ( + result []response.RecycleBinDTO + ) + partitions, err := disk.Partitions(false) + if err != nil { + return 0, nil, err + } + op := files.NewFileOp() + for _, p := range partitions { + dir := path.Join(p.Mountpoint, ".1panel_clash") + if !op.Stat(dir) { + continue + } + clashFiles, err := os.ReadDir(dir) + if err != nil { + return 0, nil, err + } + for _, file := range clashFiles { + if strings.HasPrefix(file.Name(), "_1p_") { + recycleDTO, err := getRecycleBinDTOFromName(file.Name()) + recycleDTO.IsDir = file.IsDir() + recycleDTO.From = dir + if err == nil { + result = append(result, *recycleDTO) + } + } + } + } + startIndex := (search.Page - 1) * search.PageSize + endIndex := startIndex + search.PageSize + + if startIndex > len(result) { + return int64(len(result)), result, nil + } + if endIndex > len(result) { + endIndex = len(result) + } + return int64(len(result)), result[startIndex:endIndex], nil +} + +func (r RecycleBinService) Create(create request.RecycleBinCreate) error { + op := files.NewFileOp() + if !op.Stat(create.SourcePath) { + return buserr.New(constant.ErrLinkPathNotFound) + } + clashDir, err := getClashDir(create.SourcePath) + if err != nil { + return err + } + paths := strings.Split(create.SourcePath, "/") + rNamePre := strings.Join(paths, "_1p_") + deleteTime := time.Now() + openFile, err := op.OpenFile(create.SourcePath) + if err != nil { + return err + } + fileInfo, err := openFile.Stat() + if err != nil { + return err + } + size := 0 + if fileInfo.IsDir() { + sizeF, err := op.GetDirSize(create.SourcePath) + if err != nil { + return err + } + size = int(sizeF) + } else { + size = int(fileInfo.Size()) + } + + rName := fmt.Sprintf("_1p_%s%s_p_%d_%d", "file", rNamePre, size, deleteTime.Unix()) + return op.Mv(create.SourcePath, path.Join(clashDir, rName)) +} + +func (r RecycleBinService) Reduce(reduce request.RecycleBinReduce) error { + filePath := path.Join(reduce.From, reduce.RName) + op := files.NewFileOp() + if !op.Stat(filePath) { + return buserr.New(constant.ErrLinkPathNotFound) + } + recycleBinDTO, err := getRecycleBinDTOFromName(reduce.RName) + if err != nil { + return err + } + if !op.Stat(path.Dir(recycleBinDTO.SourcePath)) { + return buserr.New("ErrSourcePathNotFound") + } + if op.Stat(recycleBinDTO.SourcePath) { + if err = op.RmRf(recycleBinDTO.SourcePath); err != nil { + return err + } + } + return op.Mv(filePath, recycleBinDTO.SourcePath) +} + +func (r RecycleBinService) Clear() error { + partitions, err := disk.Partitions(false) + if err != nil { + return err + } + op := files.NewFileOp() + for _, p := range partitions { + dir := path.Join(p.Mountpoint, ".1panel_clash") + if !op.Stat(dir) { + continue + } + newDir := path.Join(p.Mountpoint, "1panel_clash") + if err := op.Mv(dir, newDir); err != nil { + return err + } + go func() { + _ = op.DeleteDir(newDir) + }() + } + return nil +} + +func getClashDir(realPath string) (string, error) { + partitions, err := disk.Partitions(false) + if err != nil { + return "", err + } + for _, p := range partitions { + if p.Mountpoint == "/" { + continue + } + 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) +} + +func createClashDir(clashDir string) error { + op := files.NewFileOp() + if !op.Stat(clashDir) { + if err := op.CreateDir(clashDir, 0755); err != nil { + return err + } + } + return nil +} + +func getRecycleBinDTOFromName(filename string) (*response.RecycleBinDTO, error) { + r := regexp.MustCompile(`_1p_file_1p_(.+)_p_(\d+)_(\d+)`) + matches := r.FindStringSubmatch(filename) + if len(matches) != 4 { + return nil, fmt.Errorf("invalid filename format") + } + sourcePath := "/" + strings.ReplaceAll(matches[1], "_1p_", "/") + size, err := strconv.ParseInt(matches[2], 10, 64) + if err != nil { + return nil, err + } + if size < math.MinInt || size > math.MaxInt { + return nil, fmt.Errorf("size out of int range") + } + + deleteTime, err := strconv.ParseInt(matches[3], 10, 64) + if err != nil { + return nil, err + } + return &response.RecycleBinDTO{ + Name: path.Base(sourcePath), + Size: int(size), + Type: "file", + DeleteTime: time.Unix(deleteTime, 0), + SourcePath: sourcePath, + RName: filename, + }, nil +} diff --git a/agent/app/service/runtime.go b/agent/app/service/runtime.go new file mode 100644 index 000000000..4e9e4a5e9 --- /dev/null +++ b/agent/app/service/runtime.go @@ -0,0 +1,618 @@ +package service + +import ( + "context" + "encoding/json" + "fmt" + "os" + "path" + "path/filepath" + "regexp" + "strconv" + "strings" + "time" + + "github.com/1Panel-dev/1Panel/agent/app/dto" + "github.com/1Panel-dev/1Panel/agent/app/dto/request" + "github.com/1Panel-dev/1Panel/agent/app/dto/response" + "github.com/1Panel-dev/1Panel/agent/app/model" + "github.com/1Panel-dev/1Panel/agent/app/repo" + "github.com/1Panel-dev/1Panel/agent/buserr" + "github.com/1Panel-dev/1Panel/agent/constant" + "github.com/1Panel-dev/1Panel/agent/global" + cmd2 "github.com/1Panel-dev/1Panel/agent/utils/cmd" + "github.com/1Panel-dev/1Panel/agent/utils/compose" + "github.com/1Panel-dev/1Panel/agent/utils/docker" + "github.com/1Panel-dev/1Panel/agent/utils/env" + "github.com/1Panel-dev/1Panel/agent/utils/files" + "github.com/pkg/errors" + "github.com/subosito/gotenv" +) + +type RuntimeService struct { +} + +type IRuntimeService interface { + Page(req request.RuntimeSearch) (int64, []response.RuntimeDTO, error) + Create(create request.RuntimeCreate) (*model.Runtime, error) + Delete(delete request.RuntimeDelete) error + Update(req request.RuntimeUpdate) error + Get(id uint) (res *response.RuntimeDTO, err error) + GetNodePackageRunScript(req request.NodePackageReq) ([]response.PackageScripts, error) + OperateRuntime(req request.RuntimeOperate) error + GetNodeModules(req request.NodeModuleReq) ([]response.NodeModule, error) + OperateNodeModules(req request.NodeModuleOperateReq) error + SyncForRestart() error + SyncRuntimeStatus() error + DeleteCheck(installID uint) ([]dto.AppResource, error) +} + +func NewRuntimeService() IRuntimeService { + return &RuntimeService{} +} + +func (r *RuntimeService) Create(create request.RuntimeCreate) (*model.Runtime, error) { + var ( + opts []repo.DBOption + ) + if create.Name != "" { + opts = append(opts, commonRepo.WithLikeName(create.Name)) + } + if create.Type != "" { + opts = append(opts, commonRepo.WithByType(create.Type)) + } + exist, _ := runtimeRepo.GetFirst(opts...) + if exist != nil { + return nil, buserr.New(constant.ErrNameIsExist) + } + fileOp := files.NewFileOp() + + switch create.Type { + case constant.RuntimePHP: + if create.Resource == constant.ResourceLocal { + runtime := &model.Runtime{ + Name: create.Name, + Resource: create.Resource, + Type: create.Type, + Version: create.Version, + Status: constant.RuntimeNormal, + } + return nil, runtimeRepo.Create(context.Background(), runtime) + } + exist, _ = runtimeRepo.GetFirst(runtimeRepo.WithImage(create.Image)) + if exist != nil { + return nil, buserr.New(constant.ErrImageExist) + } + case constant.RuntimeNode, constant.RuntimeJava, constant.RuntimeGo: + if !fileOp.Stat(create.CodeDir) { + return nil, buserr.New(constant.ErrPathNotFound) + } + create.Install = true + if err := checkPortExist(create.Port); err != nil { + return nil, err + } + for _, export := range create.ExposedPorts { + if err := checkPortExist(export.HostPort); err != nil { + return nil, err + } + } + if containerName, ok := create.Params["CONTAINER_NAME"]; ok { + if err := checkContainerName(containerName.(string)); err != nil { + return nil, err + } + } + } + + appDetail, err := appDetailRepo.GetFirst(commonRepo.WithByID(create.AppDetailID)) + if err != nil { + return nil, err + } + app, err := appRepo.GetFirst(commonRepo.WithByID(appDetail.AppId)) + if err != nil { + return nil, err + } + + appVersionDir := filepath.Join(app.GetAppResourcePath(), appDetail.Version) + if !fileOp.Stat(appVersionDir) || appDetail.Update { + if err = downloadApp(app, appDetail, nil); err != nil { + return nil, err + } + } + + runtime := &model.Runtime{ + Name: create.Name, + AppDetailID: create.AppDetailID, + Type: create.Type, + Image: create.Image, + Resource: create.Resource, + Version: create.Version, + } + + switch create.Type { + case constant.RuntimePHP: + if err = handlePHP(create, runtime, fileOp, appVersionDir); err != nil { + return nil, err + } + case constant.RuntimeNode, constant.RuntimeJava, constant.RuntimeGo: + runtime.Port = create.Port + if err = handleNodeAndJava(create, runtime, fileOp, appVersionDir); err != nil { + return nil, err + } + } + if err := runtimeRepo.Create(context.Background(), runtime); err != nil { + return nil, err + } + return runtime, nil +} + +func (r *RuntimeService) Page(req request.RuntimeSearch) (int64, []response.RuntimeDTO, error) { + var ( + opts []repo.DBOption + res []response.RuntimeDTO + ) + if req.Name != "" { + opts = append(opts, commonRepo.WithLikeName(req.Name)) + } + if req.Status != "" { + opts = append(opts, runtimeRepo.WithStatus(req.Status)) + } + if req.Type != "" { + opts = append(opts, commonRepo.WithByType(req.Type)) + } + total, runtimes, err := runtimeRepo.Page(req.Page, req.PageSize, opts...) + if err != nil { + return 0, nil, err + } + for _, runtime := range runtimes { + runtimeDTO := response.NewRuntimeDTO(runtime) + runtimeDTO.Params = make(map[string]interface{}) + envs, err := gotenv.Unmarshal(runtime.Env) + if err != nil { + return 0, nil, err + } + for k, v := range envs { + runtimeDTO.Params[k] = v + } + res = append(res, runtimeDTO) + } + return total, res, nil +} + +func (r *RuntimeService) DeleteCheck(runTimeId uint) ([]dto.AppResource, error) { + var res []dto.AppResource + websites, _ := websiteRepo.GetBy(websiteRepo.WithRuntimeID(runTimeId)) + for _, website := range websites { + res = append(res, dto.AppResource{ + Type: "website", + Name: website.PrimaryDomain, + }) + } + return res, nil +} + +func (r *RuntimeService) Delete(runtimeDelete request.RuntimeDelete) error { + runtime, err := runtimeRepo.GetFirst(commonRepo.WithByID(runtimeDelete.ID)) + if err != nil { + return err + } + website, _ := websiteRepo.GetFirst(websiteRepo.WithRuntimeID(runtimeDelete.ID)) + if website.ID > 0 { + return buserr.New(constant.ErrDelWithWebsite) + } + if runtime.Resource == constant.ResourceAppstore { + projectDir := runtime.GetPath() + switch runtime.Type { + case constant.RuntimePHP: + client, err := docker.NewClient() + if err != nil { + return err + } + defer client.Close() + imageID, err := client.GetImageIDByName(runtime.Image) + if err != nil { + return err + } + if imageID != "" { + if err := client.DeleteImage(imageID); err != nil { + global.LOG.Errorf("delete image id [%s] error %v", imageID, err) + } + } + case constant.RuntimeNode, constant.RuntimeJava, constant.RuntimeGo: + if out, err := compose.Down(runtime.GetComposePath()); err != nil && !runtimeDelete.ForceDelete { + if out != "" { + return errors.New(out) + } + return err + } + } + if err := files.NewFileOp().DeleteDir(projectDir); err != nil && !runtimeDelete.ForceDelete { + return err + } + } + return runtimeRepo.DeleteBy(commonRepo.WithByID(runtimeDelete.ID)) +} + +func (r *RuntimeService) Get(id uint) (*response.RuntimeDTO, error) { + runtime, err := runtimeRepo.GetFirst(commonRepo.WithByID(id)) + if err != nil { + return nil, err + } + + res := response.NewRuntimeDTO(*runtime) + if runtime.Resource == constant.ResourceLocal { + return &res, nil + } + appDetail, err := appDetailRepo.GetFirst(commonRepo.WithByID(runtime.AppDetailID)) + if err != nil { + return nil, err + } + res.AppID = appDetail.AppId + switch runtime.Type { + case constant.RuntimePHP: + var ( + appForm dto.AppForm + appParams []response.AppParam + ) + if err := json.Unmarshal([]byte(runtime.Params), &appForm); err != nil { + return nil, err + } + envs, err := gotenv.Unmarshal(runtime.Env) + if err != nil { + return nil, err + } + if v, ok := envs["CONTAINER_PACKAGE_URL"]; ok { + res.Source = v + } + for _, form := range appForm.FormFields { + if v, ok := envs[form.EnvKey]; ok { + appParam := response.AppParam{ + Edit: false, + Key: form.EnvKey, + Rule: form.Rule, + Type: form.Type, + Required: form.Required, + } + if form.Edit { + appParam.Edit = true + } + appParam.LabelZh = form.LabelZh + appParam.LabelEn = form.LabelEn + appParam.Multiple = form.Multiple + appParam.Value = v + if form.Type == "select" { + if form.Multiple { + if v == "" { + appParam.Value = []string{} + } else { + appParam.Value = strings.Split(v, ",") + } + } else { + for _, fv := range form.Values { + if fv.Value == v { + appParam.ShowValue = fv.Label + break + } + } + } + appParam.Values = form.Values + } + appParams = append(appParams, appParam) + } + } + res.AppParams = appParams + case constant.RuntimeNode, constant.RuntimeJava, constant.RuntimeGo: + res.Params = make(map[string]interface{}) + envs, err := gotenv.Unmarshal(runtime.Env) + if err != nil { + return nil, err + } + for k, v := range envs { + switch k { + case "NODE_APP_PORT", "PANEL_APP_PORT_HTTP", "JAVA_APP_PORT", "GO_APP_PORT": + port, err := strconv.Atoi(v) + if err != nil { + return nil, err + } + res.Params[k] = port + default: + if strings.Contains(k, "CONTAINER_PORT") || strings.Contains(k, "HOST_PORT") { + if strings.Contains(k, "CONTAINER_PORT") { + r := regexp.MustCompile(`_(\d+)$`) + matches := r.FindStringSubmatch(k) + containerPort, err := strconv.Atoi(v) + if err != nil { + return nil, err + } + hostPort, err := strconv.Atoi(envs[fmt.Sprintf("HOST_PORT_%s", matches[1])]) + if err != nil { + return nil, err + } + res.ExposedPorts = append(res.ExposedPorts, request.ExposedPort{ + ContainerPort: containerPort, + HostPort: hostPort, + }) + } + } else { + res.Params[k] = v + } + } + } + if v, ok := envs["CONTAINER_PACKAGE_URL"]; ok { + res.Source = v + } + } + + return &res, nil +} + +func (r *RuntimeService) Update(req request.RuntimeUpdate) error { + runtime, err := runtimeRepo.GetFirst(commonRepo.WithByID(req.ID)) + if err != nil { + return err + } + if runtime.Resource == constant.ResourceLocal { + runtime.Version = req.Version + return runtimeRepo.Save(runtime) + } + oldImage := runtime.Image + switch runtime.Type { + case constant.RuntimePHP: + exist, _ := runtimeRepo.GetFirst(runtimeRepo.WithImage(req.Name), runtimeRepo.WithNotId(req.ID)) + if exist != nil { + return buserr.New(constant.ErrImageExist) + } + case constant.RuntimeNode, constant.RuntimeJava, constant.RuntimeGo: + if runtime.Port != req.Port { + if err = checkPortExist(req.Port); err != nil { + return err + } + runtime.Port = req.Port + } + for _, export := range req.ExposedPorts { + if err = checkPortExist(export.HostPort); err != nil { + return err + } + } + if containerName, ok := req.Params["CONTAINER_NAME"]; ok { + envs, err := gotenv.Unmarshal(runtime.Env) + if err != nil { + return err + } + oldContainerName := envs["CONTAINER_NAME"] + if containerName != oldContainerName { + if err := checkContainerName(containerName.(string)); err != nil { + return err + } + } + } + + appDetail, err := appDetailRepo.GetFirst(commonRepo.WithByID(runtime.AppDetailID)) + if err != nil { + return err + } + app, err := appRepo.GetFirst(commonRepo.WithByID(appDetail.AppId)) + if err != nil { + return err + } + fileOp := files.NewFileOp() + appVersionDir := path.Join(constant.AppResourceDir, app.Resource, app.Key, appDetail.Version) + if !fileOp.Stat(appVersionDir) || appDetail.Update { + if err := downloadApp(app, appDetail, nil); err != nil { + return err + } + _ = fileOp.Rename(path.Join(runtime.GetPath(), "run.sh"), path.Join(runtime.GetPath(), "run.sh.bak")) + _ = fileOp.CopyFile(path.Join(appVersionDir, "run.sh"), runtime.GetPath()) + } + } + + projectDir := path.Join(constant.RuntimeDir, runtime.Type, runtime.Name) + create := request.RuntimeCreate{ + Image: req.Image, + Type: runtime.Type, + Source: req.Source, + Params: req.Params, + CodeDir: req.CodeDir, + Version: req.Version, + NodeConfig: request.NodeConfig{ + Port: req.Port, + Install: true, + ExposedPorts: req.ExposedPorts, + }, + } + composeContent, envContent, _, err := handleParams(create, projectDir) + if err != nil { + return err + } + runtime.Env = string(envContent) + runtime.DockerCompose = string(composeContent) + + switch runtime.Type { + case constant.RuntimePHP: + runtime.Image = req.Image + runtime.Status = constant.RuntimeBuildIng + _ = runtimeRepo.Save(runtime) + client, err := docker.NewClient() + if err != nil { + return err + } + defer client.Close() + imageID, err := client.GetImageIDByName(oldImage) + if err != nil { + return err + } + go buildRuntime(runtime, imageID, req.Rebuild) + case constant.RuntimeNode, constant.RuntimeJava, constant.RuntimeGo: + runtime.Version = req.Version + runtime.CodeDir = req.CodeDir + runtime.Port = req.Port + runtime.Status = constant.RuntimeReCreating + _ = runtimeRepo.Save(runtime) + go reCreateRuntime(runtime) + } + return nil +} + +func (r *RuntimeService) GetNodePackageRunScript(req request.NodePackageReq) ([]response.PackageScripts, error) { + fileOp := files.NewFileOp() + if !fileOp.Stat(req.CodeDir) { + return nil, buserr.New(constant.ErrPathNotFound) + } + if !fileOp.Stat(path.Join(req.CodeDir, "package.json")) { + return nil, buserr.New(constant.ErrPackageJsonNotFound) + } + content, err := fileOp.GetContent(path.Join(req.CodeDir, "package.json")) + if err != nil { + return nil, err + } + var packageMap map[string]interface{} + err = json.Unmarshal(content, &packageMap) + if err != nil { + return nil, err + } + scripts, ok := packageMap["scripts"] + if !ok { + return nil, buserr.New(constant.ErrScriptsNotFound) + } + var packageScripts []response.PackageScripts + for k, v := range scripts.(map[string]interface{}) { + packageScripts = append(packageScripts, response.PackageScripts{ + Name: k, + Script: v.(string), + }) + } + return packageScripts, nil +} + +func (r *RuntimeService) OperateRuntime(req request.RuntimeOperate) error { + runtime, err := runtimeRepo.GetFirst(commonRepo.WithByID(req.ID)) + if err != nil { + return err + } + defer func() { + if err != nil { + runtime.Status = constant.RuntimeError + runtime.Message = err.Error() + _ = runtimeRepo.Save(runtime) + } + }() + switch req.Operate { + case constant.RuntimeUp: + if err = runComposeCmdWithLog(req.Operate, runtime.GetComposePath(), runtime.GetLogPath()); err != nil { + return err + } + if err = SyncRuntimeContainerStatus(runtime); err != nil { + return err + } + case constant.RuntimeDown: + if err = runComposeCmdWithLog(req.Operate, runtime.GetComposePath(), runtime.GetLogPath()); err != nil { + return err + } + runtime.Status = constant.RuntimeStopped + case constant.RuntimeRestart: + if err = runComposeCmdWithLog(constant.RuntimeDown, runtime.GetComposePath(), runtime.GetLogPath()); err != nil { + return err + } + if err = runComposeCmdWithLog(constant.RuntimeUp, runtime.GetComposePath(), runtime.GetLogPath()); err != nil { + return err + } + if err = SyncRuntimeContainerStatus(runtime); err != nil { + return err + } + } + return runtimeRepo.Save(runtime) +} + +func (r *RuntimeService) GetNodeModules(req request.NodeModuleReq) ([]response.NodeModule, error) { + runtime, err := runtimeRepo.GetFirst(commonRepo.WithByID(req.ID)) + if err != nil { + return nil, err + } + var res []response.NodeModule + nodeModulesPath := path.Join(runtime.CodeDir, "node_modules") + fileOp := files.NewFileOp() + if !fileOp.Stat(nodeModulesPath) { + return nil, buserr.New("ErrNodeModulesNotFound") + } + moduleDirs, err := os.ReadDir(nodeModulesPath) + if err != nil { + return nil, err + } + for _, moduleDir := range moduleDirs { + packagePath := path.Join(nodeModulesPath, moduleDir.Name(), "package.json") + if !fileOp.Stat(packagePath) { + continue + } + content, err := fileOp.GetContent(packagePath) + if err != nil { + continue + } + module := response.NodeModule{} + if err := json.Unmarshal(content, &module); err != nil { + continue + } + res = append(res, module) + } + return res, nil +} + +func (r *RuntimeService) OperateNodeModules(req request.NodeModuleOperateReq) error { + runtime, err := runtimeRepo.GetFirst(commonRepo.WithByID(req.ID)) + if err != nil { + return err + } + containerName, err := env.GetEnvValueByKey(runtime.GetEnvPath(), "CONTAINER_NAME") + if err != nil { + return err + } + cmd := req.PkgManager + switch req.Operate { + case constant.RuntimeInstall: + if req.PkgManager == constant.RuntimeNpm { + cmd += " install" + } else { + cmd += " add" + } + case constant.RuntimeUninstall: + if req.PkgManager == constant.RuntimeNpm { + cmd += " uninstall" + } else { + cmd += " remove" + } + case constant.RuntimeUpdate: + if req.PkgManager == constant.RuntimeNpm { + cmd += " update" + } else { + cmd += " upgrade" + } + } + cmd += " " + req.Module + return cmd2.ExecContainerScript(containerName, cmd, 5*time.Minute) +} + +func (r *RuntimeService) SyncForRestart() error { + runtimes, err := runtimeRepo.List() + if err != nil { + return err + } + for _, runtime := range runtimes { + if runtime.Status == constant.RuntimeBuildIng || runtime.Status == constant.RuntimeReCreating || runtime.Status == constant.RuntimeStarting || runtime.Status == constant.RuntimeCreating { + runtime.Status = constant.SystemRestart + runtime.Message = "System restart causing interrupt" + _ = runtimeRepo.Save(&runtime) + } + } + 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 || runtime.Type == constant.RuntimeJava || runtime.Type == constant.RuntimeGo { + _ = SyncRuntimeContainerStatus(&runtime) + } + } + return nil +} diff --git a/agent/app/service/runtime_utils.go b/agent/app/service/runtime_utils.go new file mode 100644 index 000000000..710bdbd47 --- /dev/null +++ b/agent/app/service/runtime_utils.go @@ -0,0 +1,436 @@ +package service + +import ( + "bytes" + "fmt" + "io" + "net/http" + "os" + "os/exec" + "path" + "path/filepath" + "strings" + "time" + + "github.com/1Panel-dev/1Panel/agent/app/dto/request" + "github.com/1Panel-dev/1Panel/agent/app/model" + "github.com/1Panel-dev/1Panel/agent/buserr" + "github.com/1Panel-dev/1Panel/agent/constant" + "github.com/1Panel-dev/1Panel/agent/global" + "github.com/1Panel-dev/1Panel/agent/utils/docker" + "github.com/1Panel-dev/1Panel/agent/utils/files" + httpUtil "github.com/1Panel-dev/1Panel/agent/utils/http" + "github.com/pkg/errors" + "github.com/subosito/gotenv" + "gopkg.in/yaml.v3" +) + +func handleNodeAndJava(create request.RuntimeCreate, runtime *model.Runtime, fileOp files.FileOp, appVersionDir string) (err error) { + runtimeDir := path.Join(constant.RuntimeDir, create.Type) + if err = fileOp.CopyDir(appVersionDir, runtimeDir); err != nil { + return + } + versionDir := path.Join(runtimeDir, filepath.Base(appVersionDir)) + projectDir := path.Join(runtimeDir, create.Name) + defer func() { + if err != nil { + _ = fileOp.DeleteDir(projectDir) + } + }() + if err = fileOp.Rename(versionDir, projectDir); err != nil { + return + } + composeContent, envContent, _, err := handleParams(create, projectDir) + if err != nil { + return + } + runtime.DockerCompose = string(composeContent) + runtime.Env = string(envContent) + runtime.Status = constant.RuntimeCreating + runtime.CodeDir = create.CodeDir + + nodeDetail, err := appDetailRepo.GetFirst(commonRepo.WithByID(runtime.AppDetailID)) + if err != nil { + return err + } + + go func() { + if _, _, err := httpUtil.HandleGet(nodeDetail.DownloadCallBackUrl, http.MethodGet, constant.TimeOut5s); err != nil { + global.LOG.Errorf("http request failed(handleNode), err: %v", err) + return + } + }() + go startRuntime(runtime) + + return +} + +func handlePHP(create request.RuntimeCreate, runtime *model.Runtime, fileOp files.FileOp, appVersionDir string) (err error) { + buildDir := path.Join(appVersionDir, "build") + if !fileOp.Stat(buildDir) { + return buserr.New(constant.ErrDirNotFound) + } + runtimeDir := path.Join(constant.RuntimeDir, create.Type) + tempDir := filepath.Join(runtimeDir, fmt.Sprintf("%d", time.Now().UnixNano())) + if err = fileOp.CopyDir(buildDir, tempDir); err != nil { + return + } + oldDir := path.Join(tempDir, "build") + projectDir := path.Join(runtimeDir, create.Name) + defer func() { + if err != nil { + _ = fileOp.DeleteDir(projectDir) + } + }() + if oldDir != projectDir { + if err = fileOp.Rename(oldDir, projectDir); err != nil { + return + } + if err = fileOp.DeleteDir(tempDir); err != nil { + return + } + } + composeContent, envContent, forms, err := handleParams(create, projectDir) + if err != nil { + return + } + runtime.DockerCompose = string(composeContent) + runtime.Env = string(envContent) + runtime.Params = string(forms) + runtime.Status = constant.RuntimeBuildIng + + go buildRuntime(runtime, "", false) + return +} + +func startRuntime(runtime *model.Runtime) { + if err := runComposeCmdWithLog("up", runtime.GetComposePath(), runtime.GetLogPath()); err != nil { + runtime.Status = constant.RuntimeError + runtime.Message = err.Error() + _ = runtimeRepo.Save(runtime) + return + } + + if err := SyncRuntimeContainerStatus(runtime); err != nil { + runtime.Status = constant.RuntimeError + runtime.Message = err.Error() + _ = runtimeRepo.Save(runtime) + return + } +} + +func reCreateRuntime(runtime *model.Runtime) { + var err error + defer func() { + if err != nil { + runtime.Status = constant.RuntimeError + runtime.Message = err.Error() + _ = runtimeRepo.Save(runtime) + } + }() + if err = runComposeCmdWithLog("down", runtime.GetComposePath(), runtime.GetLogPath()); err != nil { + return + } + if err = runComposeCmdWithLog("up", runtime.GetComposePath(), runtime.GetLogPath()); err != nil { + return + } + if err := SyncRuntimeContainerStatus(runtime); err != nil { + return + } +} + +func runComposeCmdWithLog(operate string, composePath string, logPath string) error { + cmd := exec.Command("docker-compose", "-f", composePath, operate) + if operate == "up" { + cmd = exec.Command("docker-compose", "-f", composePath, operate, "-d") + } + logFile, err := os.OpenFile(logPath, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0666) + if err != nil { + global.LOG.Errorf("Failed to open log file: %v", err) + return err + } + defer logFile.Close() + multiWriterStdout := io.MultiWriter(os.Stdout, logFile) + cmd.Stdout = multiWriterStdout + var stderrBuf bytes.Buffer + multiWriterStderr := io.MultiWriter(&stderrBuf, logFile, os.Stderr) + cmd.Stderr = multiWriterStderr + + err = cmd.Run() + if err != nil { + return errors.New(buserr.New(constant.ErrRuntimeStart).Error() + ":" + stderrBuf.String()) + } + return nil +} + +func SyncRuntimeContainerStatus(runtime *model.Runtime) error { + env, err := gotenv.Unmarshal(runtime.Env) + if err != nil { + return err + } + var containerNames []string + if containerName, ok := env["CONTAINER_NAME"]; !ok { + return buserr.New("ErrContainerNameNotFound") + } else { + containerNames = append(containerNames, containerName) + } + cli, err := docker.NewClient() + if err != nil { + return err + } + defer cli.Close() + containers, err := cli.ListContainersByName(containerNames) + if err != nil { + return err + } + if len(containers) == 0 { + return buserr.WithNameAndErr("ErrContainerNotFound", containerNames[0], nil) + } + container := containers[0] + + 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 + } + } + + return runtimeRepo.Save(runtime) +} + +func buildRuntime(runtime *model.Runtime, oldImageID string, rebuild bool) { + runtimePath := runtime.GetPath() + composePath := runtime.GetComposePath() + logPath := path.Join(runtimePath, "build.log") + + logFile, err := os.OpenFile(logPath, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0666) + if err != nil { + global.LOG.Errorf("failed to open log file: %v", err) + return + } + defer func() { + _ = logFile.Close() + }() + + cmd := exec.Command("docker-compose", "-f", composePath, "build") + multiWriterStdout := io.MultiWriter(os.Stdout, logFile) + cmd.Stdout = multiWriterStdout + var stderrBuf bytes.Buffer + multiWriterStderr := io.MultiWriter(&stderrBuf, logFile, os.Stderr) + cmd.Stderr = multiWriterStderr + + err = cmd.Run() + if err != nil { + runtime.Status = constant.RuntimeError + runtime.Message = buserr.New(constant.ErrImageBuildErr).Error() + ":" + stderrBuf.String() + } else { + runtime.Status = constant.RuntimeNormal + runtime.Message = "" + if oldImageID != "" { + client, err := docker.NewClient() + if err == nil { + defer client.Close() + newImageID, err := client.GetImageIDByName(runtime.Image) + if err == nil && newImageID != oldImageID { + global.LOG.Infof("delete imageID [%s] ", oldImageID) + if err := client.DeleteImage(oldImageID); err != nil { + global.LOG.Errorf("delete imageID [%s] error %v", oldImageID, err) + } else { + global.LOG.Infof("delete old image success") + } + } + } else { + global.LOG.Errorf("delete imageID [%s] error %v", oldImageID, err) + } + } + if rebuild && runtime.ID > 0 { + websites, _ := websiteRepo.GetBy(websiteRepo.WithRuntimeID(runtime.ID)) + if len(websites) > 0 { + installService := NewIAppInstalledService() + installMap := make(map[uint]string) + for _, website := range websites { + if website.AppInstallID > 0 { + installMap[website.AppInstallID] = website.PrimaryDomain + } + } + for installID, domain := range installMap { + go func(installID uint, domain string) { + global.LOG.Infof("rebuild php runtime [%s] domain [%s]", runtime.Name, domain) + if err := installService.Operate(request.AppInstalledOperate{ + InstallId: installID, + Operate: constant.Rebuild, + }); err != nil { + global.LOG.Errorf("rebuild php runtime [%s] domain [%s] error %v", runtime.Name, domain, err) + } + }(installID, domain) + } + } + } + } + _ = runtimeRepo.Save(runtime) +} + +func handleParams(create request.RuntimeCreate, projectDir string) (composeContent []byte, envContent []byte, forms []byte, err error) { + fileOp := files.NewFileOp() + composeContent, err = fileOp.GetContent(path.Join(projectDir, "docker-compose.yml")) + if err != nil { + return + } + envPath := path.Join(projectDir, ".env") + if !fileOp.Stat(envPath) { + _ = fileOp.CreateFile(envPath) + } + env, err := gotenv.Read(envPath) + if err != nil { + return + } + switch create.Type { + case constant.RuntimePHP: + create.Params["IMAGE_NAME"] = create.Image + forms, err = fileOp.GetContent(path.Join(projectDir, "config.json")) + if err != nil { + return + } + if extends, ok := create.Params["PHP_EXTENSIONS"]; ok { + if extendsArray, ok := extends.([]interface{}); ok { + strArray := make([]string, len(extendsArray)) + for i, v := range extendsArray { + strArray[i] = strings.ToLower(fmt.Sprintf("%v", v)) + } + create.Params["PHP_EXTENSIONS"] = strings.Join(strArray, ",") + } + } + create.Params["CONTAINER_PACKAGE_URL"] = create.Source + case constant.RuntimeNode: + create.Params["CODE_DIR"] = create.CodeDir + create.Params["NODE_VERSION"] = create.Version + create.Params["PANEL_APP_PORT_HTTP"] = create.Port + if create.NodeConfig.Install { + create.Params["RUN_INSTALL"] = "1" + } else { + create.Params["RUN_INSTALL"] = "0" + } + create.Params["CONTAINER_PACKAGE_URL"] = create.Source + + composeContent, err = handleCompose(env, composeContent, create, projectDir) + if err != nil { + return + } + case constant.RuntimeJava: + create.Params["CODE_DIR"] = create.CodeDir + create.Params["JAVA_VERSION"] = create.Version + create.Params["PANEL_APP_PORT_HTTP"] = create.Port + composeContent, err = handleCompose(env, composeContent, create, projectDir) + if err != nil { + return + } + case constant.RuntimeGo: + create.Params["CODE_DIR"] = create.CodeDir + create.Params["GO_VERSION"] = create.Version + create.Params["PANEL_APP_PORT_HTTP"] = create.Port + composeContent, err = handleCompose(env, composeContent, create, projectDir) + if err != nil { + return + } + } + + newMap := make(map[string]string) + handleMap(create.Params, newMap) + for k, v := range newMap { + env[k] = v + } + + envStr, err := gotenv.Marshal(env) + if err != nil { + return + } + if err = gotenv.Write(env, envPath); err != nil { + return + } + envContent = []byte(envStr) + return +} + +func handleCompose(env gotenv.Env, composeContent []byte, create request.RuntimeCreate, projectDir string) (composeByte []byte, err error) { + existMap := make(map[string]interface{}) + composeMap := make(map[string]interface{}) + if err = yaml.Unmarshal(composeContent, &composeMap); err != nil { + return + } + services, serviceValid := composeMap["services"].(map[string]interface{}) + if !serviceValid { + err = buserr.New(constant.ErrFileParse) + return + } + serviceName := "" + serviceValue := make(map[string]interface{}) + for name, service := range services { + serviceName = name + serviceValue = service.(map[string]interface{}) + _, ok := serviceValue["ports"].([]interface{}) + if ok { + var ports []interface{} + + switch create.Type { + case constant.RuntimeNode: + ports = append(ports, "${HOST_IP}:${PANEL_APP_PORT_HTTP}:${NODE_APP_PORT}") + case constant.RuntimeJava: + ports = append(ports, "${HOST_IP}:${PANEL_APP_PORT_HTTP}:${JAVA_APP_PORT}") + case constant.RuntimeGo: + ports = append(ports, "${HOST_IP}:${PANEL_APP_PORT_HTTP}:${GO_APP_PORT}") + + } + + for i, port := range create.ExposedPorts { + containerPortStr := fmt.Sprintf("CONTAINER_PORT_%d", i) + hostPortStr := fmt.Sprintf("HOST_PORT_%d", i) + existMap[containerPortStr] = struct{}{} + existMap[hostPortStr] = struct{}{} + ports = append(ports, fmt.Sprintf("${HOST_IP}:${%s}:${%s}", hostPortStr, containerPortStr)) + create.Params[containerPortStr] = port.ContainerPort + create.Params[hostPortStr] = port.HostPort + } + serviceValue["ports"] = ports + } + break + } + for k := range env { + if strings.Contains(k, "CONTAINER_PORT_") || strings.Contains(k, "HOST_PORT_") { + if _, ok := existMap[k]; !ok { + delete(env, k) + } + } + } + + services[serviceName] = serviceValue + composeMap["services"] = services + composeByte, err = yaml.Marshal(composeMap) + if err != nil { + return + } + fileOp := files.NewFileOp() + _ = fileOp.SaveFile(path.Join(projectDir, "docker-compose.yml"), string(composeByte), 0644) + return +} + +func checkContainerName(name string) error { + dockerCli, err := docker.NewClient() + if err != nil { + return err + } + defer dockerCli.Close() + names, err := dockerCli.ListContainersByName([]string{name}) + if err != nil { + return err + } + if len(names) > 0 { + return buserr.New(constant.ErrContainerName) + } + return nil +} diff --git a/agent/app/service/setting.go b/agent/app/service/setting.go new file mode 100644 index 000000000..5d74d86fb --- /dev/null +++ b/agent/app/service/setting.go @@ -0,0 +1,86 @@ +package service + +import ( + "encoding/json" + "time" + + "github.com/1Panel-dev/1Panel/agent/app/dto" + "github.com/1Panel-dev/1Panel/agent/constant" + "github.com/1Panel-dev/1Panel/agent/global" + "github.com/robfig/cron/v3" +) + +type SettingService struct{} + +type ISettingService interface { + GetSettingInfo() (*dto.SettingInfo, error) + Update(key, value string) error +} + +func NewISettingService() ISettingService { + return &SettingService{} +} + +func (u *SettingService) GetSettingInfo() (*dto.SettingInfo, error) { + setting, err := settingRepo.GetList() + if err != nil { + return nil, constant.ErrRecordNotFound + } + settingMap := make(map[string]string) + for _, set := range setting { + settingMap[set.Key] = set.Value + } + var info dto.SettingInfo + arr, err := json.Marshal(settingMap) + if err != nil { + return nil, err + } + if err := json.Unmarshal(arr, &info); err != nil { + return nil, err + } + + info.LocalTime = time.Now().Format("2006-01-02 15:04:05 MST -0700") + return &info, err +} + +func (u *SettingService) Update(key, value string) error { + switch key { + case "MonitorStatus": + if value == "enable" && global.MonitorCronID == 0 { + interval, err := settingRepo.Get(settingRepo.WithByKey("MonitorInterval")) + if err != nil { + return err + } + if err := StartMonitor(false, interval.Value); err != nil { + return err + } + } + if value == "disable" && global.MonitorCronID != 0 { + monitorCancel() + global.Cron.Remove(cron.EntryID(global.MonitorCronID)) + global.MonitorCronID = 0 + } + case "MonitorInterval": + status, err := settingRepo.Get(settingRepo.WithByKey("MonitorStatus")) + if err != nil { + return err + } + if status.Value == "enable" && global.MonitorCronID != 0 { + if err := StartMonitor(true, value); err != nil { + return err + } + } + case "AppStoreLastModified": + exist, _ := settingRepo.Get(settingRepo.WithByKey("AppStoreLastModified")) + if exist.ID == 0 { + _ = settingRepo.Create("AppStoreLastModified", value) + return nil + } + } + + if err := settingRepo.Update(key, value); err != nil { + return err + } + + return nil +} diff --git a/agent/app/service/snapshot.go b/agent/app/service/snapshot.go new file mode 100644 index 000000000..a674a5091 --- /dev/null +++ b/agent/app/service/snapshot.go @@ -0,0 +1,556 @@ +package service + +import ( + "context" + "encoding/json" + "fmt" + "os" + "path" + "strings" + "sync" + "time" + + "github.com/1Panel-dev/1Panel/agent/app/dto" + "github.com/1Panel-dev/1Panel/agent/app/model" + "github.com/1Panel-dev/1Panel/agent/constant" + "github.com/1Panel-dev/1Panel/agent/global" + "github.com/1Panel-dev/1Panel/agent/utils/cmd" + "github.com/1Panel-dev/1Panel/agent/utils/compose" + "github.com/1Panel-dev/1Panel/agent/utils/files" + "github.com/jinzhu/copier" + "github.com/pkg/errors" + "github.com/shirou/gopsutil/v3/host" +) + +type SnapshotService struct { + OriginalPath string +} + +type ISnapshotService interface { + SearchWithPage(req dto.SearchWithPage) (int64, interface{}, error) + SnapshotCreate(req dto.SnapshotCreate) error + SnapshotRecover(req dto.SnapshotRecover) error + SnapshotRollback(req dto.SnapshotRecover) error + SnapshotImport(req dto.SnapshotImport) error + Delete(req dto.SnapshotBatchDelete) error + + LoadSnapShotStatus(id uint) (*dto.SnapshotStatus, error) + + UpdateDescription(req dto.UpdateDescription) error + readFromJson(path string) (SnapshotJson, error) + + HandleSnapshot(isCronjob bool, logPath string, req dto.SnapshotCreate, timeNow string, secret string) (string, error) +} + +func NewISnapshotService() ISnapshotService { + return &SnapshotService{} +} + +func (u *SnapshotService) SearchWithPage(req dto.SearchWithPage) (int64, interface{}, error) { + total, systemBackups, err := snapshotRepo.Page(req.Page, req.PageSize, commonRepo.WithLikeName(req.Info)) + if err != nil { + return 0, nil, err + } + dtoSnap, err := loadSnapSize(systemBackups) + if err != nil { + return 0, nil, err + } + return total, dtoSnap, err +} + +func (u *SnapshotService) SnapshotImport(req dto.SnapshotImport) error { + if len(req.Names) == 0 { + return fmt.Errorf("incorrect snapshot request body: %v", req.Names) + } + for _, snapName := range req.Names { + snap, _ := snapshotRepo.Get(commonRepo.WithByName(strings.ReplaceAll(snapName, ".tar.gz", ""))) + if snap.ID != 0 { + return constant.ErrRecordExist + } + } + 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 { + return fmt.Errorf("incorrect snapshot name format of %s", shortName) + } + if strings.HasSuffix(snap, ".tar.gz") { + snap = strings.ReplaceAll(snap, ".tar.gz", "") + } + itemSnap := model.Snapshot{ + Name: snap, + From: req.From, + DefaultDownload: req.From, + Version: nameItems[1], + Description: req.Description, + Status: constant.StatusSuccess, + } + if err := snapshotRepo.Create(&itemSnap); err != nil { + return err + } + } + return nil +} + +func (u *SnapshotService) UpdateDescription(req dto.UpdateDescription) error { + return snapshotRepo.Update(req.ID, map[string]interface{}{"description": req.Description}) +} + +func (u *SnapshotService) LoadSnapShotStatus(id uint) (*dto.SnapshotStatus, error) { + var data dto.SnapshotStatus + status, err := snapshotRepo.GetStatus(id) + if err != nil { + return nil, err + } + if err := copier.Copy(&data, &status); err != nil { + return nil, errors.WithMessage(constant.ErrStructTransform, err.Error()) + } + return &data, nil +} + +type SnapshotJson struct { + OldBaseDir string `json:"oldBaseDir"` + OldDockerDataDir string `json:"oldDockerDataDir"` + OldBackupDataDir string `json:"oldBackupDataDir"` + OldPanelDataDir string `json:"oldPanelDataDir"` + + BaseDir string `json:"baseDir"` + DockerDataDir string `json:"dockerDataDir"` + BackupDataDir string `json:"backupDataDir"` + PanelDataDir string `json:"panelDataDir"` + LiveRestoreEnabled bool `json:"liveRestoreEnabled"` +} + +func (u *SnapshotService) SnapshotCreate(req dto.SnapshotCreate) error { + if _, err := u.HandleSnapshot(false, "", req, time.Now().Format(constant.DateTimeSlimLayout), req.Secret); err != nil { + return err + } + return nil +} + +func (u *SnapshotService) SnapshotRecover(req dto.SnapshotRecover) error { + global.LOG.Info("start to recover panel by snapshot now") + snap, err := snapshotRepo.Get(commonRepo.WithByID(req.ID)) + 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") + } + + 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) + } + + _ = snapshotRepo.Update(snap.ID, map[string]interface{}{"recover_status": constant.StatusWaiting}) + _ = settingRepo.Update("SystemStatus", "Recovering") + go u.HandleSnapshotRecover(snap, true, req) + return nil +} + +func (u *SnapshotService) SnapshotRollback(req dto.SnapshotRecover) error { + global.LOG.Info("start to rollback now") + snap, err := snapshotRepo.Get(commonRepo.WithByID(req.ID)) + if err != nil { + return err + } + req.IsNew = false + snap.InterruptStep = "Readjson" + go u.HandleSnapshotRecover(snap, false, req) + return nil +} + +func (u *SnapshotService) readFromJson(path string) (SnapshotJson, error) { + var snap SnapshotJson + if _, err := os.Stat(path); err != nil { + return snap, fmt.Errorf("find snapshot json file in recover package failed, err: %v", err) + } + fileByte, err := os.ReadFile(path) + if err != nil { + return snap, fmt.Errorf("read file from path %s failed, err: %v", path, err) + } + if err := json.Unmarshal(fileByte, &snap); err != nil { + return snap, fmt.Errorf("unmarshal snapjson failed, err: %v", err) + } + return snap, nil +} + +func (u *SnapshotService) HandleSnapshot(isCronjob bool, logPath string, req dto.SnapshotCreate, timeNow string, secret string) (string, error) { + localDir, err := loadLocalDir() + if err != nil { + return "", err + } + var ( + rootDir string + snap model.Snapshot + snapStatus model.SnapshotStatus + ) + + if req.ID == 0 { + versionItem, _ := settingRepo.Get(settingRepo.WithByKey("SystemVersion")) + + name := fmt.Sprintf("1panel_%s_%s_%s", versionItem.Value, loadOs(), timeNow) + if isCronjob { + name = fmt.Sprintf("snapshot_1panel_%s_%s_%s", versionItem.Value, loadOs(), timeNow) + } + rootDir = path.Join(localDir, "system", name) + + snap = model.Snapshot{ + Name: name, + Description: req.Description, + From: req.From, + DefaultDownload: req.DefaultDownload, + Version: versionItem.Value, + Status: constant.StatusWaiting, + } + _ = snapshotRepo.Create(&snap) + snapStatus.SnapID = snap.ID + _ = snapshotRepo.CreateStatus(&snapStatus) + } else { + snap, err = snapshotRepo.Get(commonRepo.WithByID(req.ID)) + 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 + _ = snapshotRepo.CreateStatus(&snapStatus) + } + rootDir = path.Join(localDir, fmt.Sprintf("system/%s", snap.Name)) + } + + var wg sync.WaitGroup + itemHelper := snapHelper{SnapID: snap.ID, Status: &snapStatus, Wg: &wg, FileOp: files.NewFileOp(), Ctx: context.Background()} + backupPanelDir := path.Join(rootDir, "1panel") + _ = os.MkdirAll(backupPanelDir, os.ModePerm) + backupDockerDir := path.Join(rootDir, "docker") + _ = os.MkdirAll(backupDockerDir, os.ModePerm) + + jsonItem := SnapshotJson{ + BaseDir: global.CONF.System.BaseDir, + BackupDataDir: localDir, + PanelDataDir: path.Join(global.CONF.System.BaseDir, "1panel"), + } + loadLogByStatus(snapStatus, logPath) + if snapStatus.PanelInfo != constant.StatusDone { + wg.Add(1) + go snapJson(itemHelper, jsonItem, rootDir) + } + if snapStatus.Panel != constant.StatusDone { + wg.Add(1) + go snapPanel(itemHelper, backupPanelDir) + } + if snapStatus.DaemonJson != constant.StatusDone { + wg.Add(1) + go snapDaemonJson(itemHelper, backupDockerDir) + } + if snapStatus.AppData != constant.StatusDone { + wg.Add(1) + go snapAppData(itemHelper, backupDockerDir) + } + if snapStatus.BackupData != constant.StatusDone { + wg.Add(1) + go snapBackup(itemHelper, localDir, backupPanelDir) + } + + if !isCronjob { + go func() { + wg.Wait() + if !checkIsAllDone(snap.ID) { + _ = snapshotRepo.Update(snap.ID, map[string]interface{}{"status": constant.StatusFailed}) + return + } + 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 + } + if snapStatus.Compress != constant.StatusDone { + snapCompress(itemHelper, rootDir, secret) + } + if snapStatus.Compress != constant.StatusDone { + _ = snapshotRepo.Update(snap.ID, map[string]interface{}{"status": constant.StatusFailed}) + return + } + 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 + } + _ = snapshotRepo.Update(snap.ID, map[string]interface{}{"status": constant.StatusSuccess}) + }() + return "", nil + } + wg.Wait() + if !checkIsAllDone(snap.ID) { + _ = snapshotRepo.Update(snap.ID, map[string]interface{}{"status": constant.StatusFailed}) + loadLogByStatus(snapStatus, logPath) + return snap.Name, fmt.Errorf("snapshot %s backup failed", snap.Name) + } + loadLogByStatus(snapStatus, logPath) + snapPanelData(itemHelper, localDir, backupPanelDir) + if snapStatus.PanelData != constant.StatusDone { + _ = snapshotRepo.Update(snap.ID, map[string]interface{}{"status": constant.StatusFailed}) + loadLogByStatus(snapStatus, logPath) + return snap.Name, fmt.Errorf("snapshot %s 1panel data failed", snap.Name) + } + loadLogByStatus(snapStatus, logPath) + snapCompress(itemHelper, rootDir, secret) + if snapStatus.Compress != constant.StatusDone { + _ = snapshotRepo.Update(snap.ID, map[string]interface{}{"status": constant.StatusFailed}) + loadLogByStatus(snapStatus, logPath) + return snap.Name, fmt.Errorf("snapshot %s compress failed", snap.Name) + } + loadLogByStatus(snapStatus, logPath) + 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}) + loadLogByStatus(snapStatus, logPath) + return snap.Name, fmt.Errorf("snapshot %s upload failed", snap.Name) + } + _ = snapshotRepo.Update(snap.ID, map[string]interface{}{"status": constant.StatusSuccess}) + loadLogByStatus(snapStatus, logPath) + return snap.Name, nil +} + +func (u *SnapshotService) Delete(req dto.SnapshotBatchDelete) error { + snaps, _ := snapshotRepo.GetList(commonRepo.WithIdsIn(req.Ids)) + for _, snap := range snaps { + if req.DeleteWithFile { + targetAccounts, err := loadClientMap(snap.From) + if err != nil { + return err + } + for _, item := range targetAccounts { + global.LOG.Debugf("remove snapshot file %s.tar.gz from %s", snap.Name, item.backType) + _, _ = item.client.Delete(path.Join(item.backupPath, "system_snapshot", snap.Name+".tar.gz")) + } + } + + _ = snapshotRepo.DeleteStatus(snap.ID) + if err := snapshotRepo.Delete(commonRepo.WithByID(snap.ID)); err != nil { + return err + } + } + return nil +} + +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(constant.DateTimeLayout), + }); err != nil { + global.LOG.Errorf("update snap recover status failed, err: %v", err) + } + _ = settingRepo.Update("SystemStatus", "Free") + return + } + _ = settingRepo.Update("SystemStatus", "Free") + if status == constant.StatusSuccess { + if err := snapshotRepo.Update(id, map[string]interface{}{ + "recover_status": "", + "recover_message": "", + "interrupt_step": "", + "rollback_status": "", + "rollback_message": "", + "last_rollbacked_at": time.Now().Format(constant.DateTimeLayout), + }); err != nil { + global.LOG.Errorf("update snap recover status failed, err: %v", err) + } + return + } + global.LOG.Errorf("rollback failed, err: %s", message) + if err := snapshotRepo.Update(id, map[string]interface{}{ + "rollback_status": status, + "rollback_message": message, + "last_rollbacked_at": time.Now().Format(constant.DateTimeLayout), + }); err != nil { + global.LOG.Errorf("update snap recover status failed, err: %v", err) + } +} + +func (u *SnapshotService) handleUnTar(sourceDir, targetDir string, secret string) error { + if _, err := os.Stat(targetDir); err != nil && os.IsNotExist(err) { + if err = os.MkdirAll(targetDir, os.ModePerm); err != nil { + return err + } + } + commands := "" + if len(secret) != 0 { + extraCmd := "openssl enc -d -aes-256-cbc -k '" + secret + "' -in " + sourceDir + " | " + commands = fmt.Sprintf("%s tar -zxvf - -C %s", extraCmd, targetDir+" > /dev/null 2>&1") + global.LOG.Debug(strings.ReplaceAll(commands, fmt.Sprintf(" %s ", secret), "******")) + } else { + commands = fmt.Sprintf("tar zxvfC %s %s", sourceDir, targetDir) + global.LOG.Debug(commands) + } + stdout, err := cmd.ExecWithTimeOut(commands, 30*time.Minute) + if err != nil { + if len(stdout) != 0 { + global.LOG.Errorf("do handle untar failed, stdout: %s, err: %v", stdout, err) + return fmt.Errorf("do handle untar failed, stdout: %s, err: %v", stdout, err) + } + } + return nil +} + +func rebuildAllAppInstall() error { + global.LOG.Debug("start to rebuild all app") + appInstalls, err := appInstallRepo.ListBy() + if err != nil { + global.LOG.Errorf("get all app installed for rebuild failed, err: %v", err) + return err + } + var wg sync.WaitGroup + for i := 0; i < len(appInstalls); i++ { + wg.Add(1) + appInstalls[i].Status = constant.Rebuilding + _ = appInstallRepo.Save(context.Background(), &appInstalls[i]) + go func(app model.AppInstall) { + defer wg.Done() + dockerComposePath := app.GetComposePath() + out, err := compose.Down(dockerComposePath) + if err != nil { + _ = handleErr(app, err, out) + return + } + out, err = compose.Up(dockerComposePath) + if err != nil { + _ = handleErr(app, err, out) + return + } + app.Status = constant.Running + _ = appInstallRepo.Save(context.Background(), &app) + }(appInstalls[i]) + } + wg.Wait() + return nil +} + +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, status.Panel + } + if status.PanelInfo != constant.StatusDone { + return false, status.PanelInfo + } + if status.DaemonJson != constant.StatusDone { + return false, status.DaemonJson + } + if status.AppData != constant.StatusDone { + return false, status.AppData + } + if status.BackupData != constant.StatusDone { + return false, status.BackupData + } + return true, "" +} + +func loadLogByStatus(status model.SnapshotStatus, logPath string) { + logs := "" + logs += fmt.Sprintf("Write 1Panel basic information: %s \n", status.PanelInfo) + logs += fmt.Sprintf("Backup 1Panel system files: %s \n", status.Panel) + logs += fmt.Sprintf("Backup Docker configuration file: %s \n", status.DaemonJson) + logs += fmt.Sprintf("Backup installed apps from 1Panel: %s \n", status.AppData) + logs += fmt.Sprintf("Backup 1Panel data directory: %s \n", status.PanelData) + logs += fmt.Sprintf("Backup local backup directory for 1Panel: %s \n", status.BackupData) + logs += fmt.Sprintf("Create snapshot file: %s \n", status.Compress) + logs += fmt.Sprintf("Snapshot size: %s \n", status.Size) + logs += fmt.Sprintf("Upload snapshot file: %s \n", status.Upload) + + file, err := os.OpenFile(logPath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0666) + if err != nil { + 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 + } +} + +func loadSnapSize(records []model.Snapshot) ([]dto.SnapshotInfo, error) { + var datas []dto.SnapshotInfo + clientMap := make(map[string]loadSizeHelper) + var wg sync.WaitGroup + for i := 0; i < len(records); i++ { + var item dto.SnapshotInfo + if err := copier.Copy(&item, &records[i]); err != nil { + return nil, errors.WithMessage(constant.ErrStructTransform, err.Error()) + } + itemPath := fmt.Sprintf("system_snapshot/%s.tar.gz", item.Name) + if _, ok := clientMap[records[i].DefaultDownload]; !ok { + backup, err := backupRepo.Get(commonRepo.WithByType(records[i].DefaultDownload)) + if err != nil { + global.LOG.Errorf("load backup model %s from db failed, err: %v", records[i].DefaultDownload, err) + clientMap[records[i].DefaultDownload] = loadSizeHelper{} + datas = append(datas, item) + continue + } + client, err := NewIBackupService().NewClient(&backup) + if err != nil { + global.LOG.Errorf("load backup client %s from db failed, err: %v", records[i].DefaultDownload, err) + clientMap[records[i].DefaultDownload] = loadSizeHelper{} + datas = append(datas, item) + continue + } + item.Size, _ = client.Size(path.Join(strings.TrimLeft(backup.BackupPath, "/"), itemPath)) + datas = append(datas, item) + clientMap[records[i].DefaultDownload] = loadSizeHelper{backupPath: strings.TrimLeft(backup.BackupPath, "/"), client: client, isOk: true} + continue + } + if clientMap[records[i].DefaultDownload].isOk { + wg.Add(1) + go func(index int) { + item.Size, _ = clientMap[records[index].DefaultDownload].client.Size(path.Join(clientMap[records[index].DefaultDownload].backupPath, itemPath)) + datas = append(datas, item) + wg.Done() + }(i) + } else { + datas = append(datas, item) + } + } + wg.Wait() + return datas, nil +} diff --git a/agent/app/service/snapshot_create.go b/agent/app/service/snapshot_create.go new file mode 100644 index 000000000..213258765 --- /dev/null +++ b/agent/app/service/snapshot_create.go @@ -0,0 +1,280 @@ +package service + +import ( + "context" + "encoding/json" + "fmt" + "os" + "path" + "regexp" + "strings" + "sync" + "time" + + "github.com/1Panel-dev/1Panel/agent/app/model" + "github.com/1Panel-dev/1Panel/agent/constant" + "github.com/1Panel-dev/1Panel/agent/global" + "github.com/1Panel-dev/1Panel/agent/utils/cmd" + "github.com/1Panel-dev/1Panel/agent/utils/common" + "github.com/1Panel-dev/1Panel/agent/utils/files" +) + +type snapHelper struct { + SnapID uint + Status *model.SnapshotStatus + Ctx context.Context + FileOp files.FileOp + Wg *sync.WaitGroup +} + +func snapJson(snap snapHelper, snapJson SnapshotJson, targetDir string) { + defer snap.Wg.Done() + _ = snapshotRepo.UpdateStatus(snap.Status.ID, map[string]interface{}{"panel_info": constant.Running}) + status := constant.StatusDone + remarkInfo, _ := json.MarshalIndent(snapJson, "", "\t") + if err := os.WriteFile(fmt.Sprintf("%s/snapshot.json", targetDir), remarkInfo, 0640); err != nil { + status = err.Error() + } + snap.Status.PanelInfo = status + _ = snapshotRepo.UpdateStatus(snap.Status.ID, map[string]interface{}{"panel_info": status}) +} + +func snapPanel(snap snapHelper, targetDir string) { + defer snap.Wg.Done() + _ = snapshotRepo.UpdateStatus(snap.Status.ID, map[string]interface{}{"panel": constant.Running}) + status := constant.StatusDone + if err := common.CopyFile("/usr/local/bin/1panel", path.Join(targetDir, "1panel")); err != nil { + status = err.Error() + } + + if err := common.CopyFile("/usr/local/bin/1pctl", targetDir); err != nil { + status = err.Error() + } + + if err := common.CopyFile("/etc/systemd/system/1panel.service", targetDir); err != nil { + status = err.Error() + } + snap.Status.Panel = status + _ = snapshotRepo.UpdateStatus(snap.Status.ID, map[string]interface{}{"panel": status}) +} + +func snapDaemonJson(snap snapHelper, targetDir string) { + defer snap.Wg.Done() + status := constant.StatusDone + if !snap.FileOp.Stat("/etc/docker/daemon.json") { + snap.Status.DaemonJson = status + _ = snapshotRepo.UpdateStatus(snap.Status.ID, map[string]interface{}{"daemon_json": status}) + return + } + _ = snapshotRepo.UpdateStatus(snap.Status.ID, map[string]interface{}{"daemon_json": constant.Running}) + if err := common.CopyFile("/etc/docker/daemon.json", targetDir); err != nil { + status = err.Error() + } + snap.Status.DaemonJson = status + _ = snapshotRepo.UpdateStatus(snap.Status.ID, map[string]interface{}{"daemon_json": status}) +} + +func snapAppData(snap snapHelper, targetDir string) { + defer snap.Wg.Done() + _ = snapshotRepo.UpdateStatus(snap.Status.ID, map[string]interface{}{"app_data": constant.Running}) + appInstalls, err := appInstallRepo.ListBy() + if err != nil { + snap.Status.AppData = err.Error() + _ = snapshotRepo.UpdateStatus(snap.Status.ID, map[string]interface{}{"app_data": err.Error()}) + return + } + runtimes, err := runtimeRepo.List() + if err != nil { + snap.Status.AppData = err.Error() + _ = snapshotRepo.UpdateStatus(snap.Status.ID, map[string]interface{}{"app_data": err.Error()}) + return + } + imageRegex := regexp.MustCompile(`image:\s*(.*)`) + var imageSaveList []string + existStr, _ := cmd.Exec("docker images | awk '{print $1\":\"$2}' | grep -v REPOSITORY:TAG") + existImages := strings.Split(existStr, "\n") + duplicateMap := make(map[string]bool) + for _, app := range appInstalls { + matches := imageRegex.FindAllStringSubmatch(app.DockerCompose, -1) + for _, match := range matches { + for _, existImage := range existImages { + if match[1] == existImage && !duplicateMap[match[1]] { + imageSaveList = append(imageSaveList, match[1]) + duplicateMap[match[1]] = true + } + } + } + } + for _, runtime := range runtimes { + for _, existImage := range existImages { + if runtime.Image == existImage && !duplicateMap[runtime.Image] { + imageSaveList = append(imageSaveList, runtime.Image) + duplicateMap[runtime.Image] = true + } + } + } + + if len(imageSaveList) != 0 { + global.LOG.Debugf("docker save %s | gzip -c > %s", strings.Join(imageSaveList, " "), path.Join(targetDir, "docker_image.tar")) + std, err := cmd.Execf("docker save %s | gzip -c > %s", strings.Join(imageSaveList, " "), path.Join(targetDir, "docker_image.tar")) + if err != nil { + snap.Status.AppData = err.Error() + _ = snapshotRepo.UpdateStatus(snap.Status.ID, map[string]interface{}{"app_data": std}) + return + } + } + snap.Status.AppData = constant.StatusDone + _ = snapshotRepo.UpdateStatus(snap.Status.ID, map[string]interface{}{"app_data": constant.StatusDone}) +} + +func snapBackup(snap snapHelper, localDir, targetDir string) { + defer snap.Wg.Done() + _ = snapshotRepo.UpdateStatus(snap.Status.ID, map[string]interface{}{"backup_data": constant.Running}) + status := constant.StatusDone + if err := handleSnapTar(localDir, targetDir, "1panel_backup.tar.gz", "./system;./system_snapshot;", ""); err != nil { + status = err.Error() + } + snap.Status.BackupData = status + _ = snapshotRepo.UpdateStatus(snap.Status.ID, map[string]interface{}{"backup_data": status}) +} + +func snapPanelData(snap snapHelper, localDir, targetDir string) { + _ = snapshotRepo.UpdateStatus(snap.Status.ID, map[string]interface{}{"panel_data": constant.Running}) + status := constant.StatusDone + dataDir := path.Join(global.CONF.System.BaseDir, "1panel") + exclusionRules := "./tmp;./log;./cache;./db/1Panel.db-*;" + 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() + } + _ = snapshotRepo.Update(snap.SnapID, map[string]interface{}{"status": constant.StatusWaiting}) + + 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, secret string) { + _ = 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)) + if err := handleSnapTar(rootDir, tmpDir, fileName, "", secret); err != nil { + snap.Status.Compress = err.Error() + _ = snapshotRepo.UpdateStatus(snap.Status.ID, map[string]interface{}{"compress": err.Error()}) + return + } + + stat, err := os.Stat(path.Join(tmpDir, fileName)) + if err != nil { + snap.Status.Compress = err.Error() + _ = snapshotRepo.UpdateStatus(snap.Status.ID, map[string]interface{}{"compress": err.Error()}) + return + } + size := common.LoadSizeUnit2F(float64(stat.Size())) + global.LOG.Debugf("compress successful! size of file: %s", size) + 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)) + _ = snapshotRepo.UpdateStatus(snap.Status.ID, map[string]interface{}{"upload": constant.StatusUploading}) + accountMap, err := loadClientMap(accounts) + if err != nil { + snap.Status.Upload = err.Error() + _ = snapshotRepo.UpdateStatus(snap.Status.ID, map[string]interface{}{"upload": err.Error()}) + return + } + targetAccounts := strings.Split(accounts, ",") + for _, item := range targetAccounts { + global.LOG.Debugf("start upload snapshot to %s, path: %s", item, path.Join(accountMap[item].backupPath, "system_snapshot", path.Base(file))) + if _, err := accountMap[item].client.Upload(source, path.Join(accountMap[item].backupPath, "system_snapshot", path.Base(file))); err != nil { + global.LOG.Debugf("upload to %s failed, err: %v", item, err) + snap.Status.Upload = err.Error() + _ = snapshotRepo.UpdateStatus(snap.Status.ID, map[string]interface{}{"upload": err.Error()}) + return + } + global.LOG.Debugf("upload to %s successful", item) + } + 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, secret string) error { + if _, err := os.Stat(targetDir); err != nil && os.IsNotExist(err) { + if err = os.MkdirAll(targetDir, os.ModePerm); err != nil { + return err + } + } + + exMap := make(map[string]struct{}) + exStr := "" + excludes := strings.Split(exclusionRules, ";") + excludes = append(excludes, "*.sock") + for _, exclude := range excludes { + if len(exclude) == 0 { + continue + } + if _, ok := exMap[exclude]; ok { + continue + } + exStr += " --exclude " + exStr += exclude + exMap[exclude] = struct{}{} + } + path := "" + if strings.Contains(sourceDir, "/") { + itemDir := strings.ReplaceAll(sourceDir[strings.LastIndex(sourceDir, "/"):], "/", "") + aheadDir := sourceDir[:strings.LastIndex(sourceDir, "/")] + if len(aheadDir) == 0 { + aheadDir = "/" + } + path += fmt.Sprintf("-C %s %s", aheadDir, itemDir) + } else { + path = sourceDir + } + commands := "" + if len(secret) != 0 { + extraCmd := "| openssl enc -aes-256-cbc -salt -k '" + secret + "' -out" + commands = fmt.Sprintf("tar --warning=no-file-changed --ignore-failed-read -zcf %s %s %s %s", " -"+exStr, path, extraCmd, targetDir+"/"+name) + global.LOG.Debug(strings.ReplaceAll(commands, fmt.Sprintf(" %s ", secret), "******")) + } else { + commands = fmt.Sprintf("tar --warning=no-file-changed --ignore-failed-read -zcf %s %s -C %s .", targetDir+"/"+name, exStr, sourceDir) + global.LOG.Debug(commands) + } + stdout, err := cmd.ExecWithTimeOut(commands, 30*time.Minute) + if err != nil { + if len(stdout) != 0 { + global.LOG.Errorf("do handle tar failed, stdout: %s, err: %v", stdout, err) + return fmt.Errorf("do handle tar failed, stdout: %s, err: %v", stdout, err) + } + } + return nil +} + +func checkPointOfWal() { + if err := global.DB.Exec("PRAGMA wal_checkpoint(TRUNCATE);").Error; err != nil { + global.LOG.Errorf("handle check point failed, err: %v", err) + } +} diff --git a/agent/app/service/snapshot_recover.go b/agent/app/service/snapshot_recover.go new file mode 100644 index 000000000..0f095192d --- /dev/null +++ b/agent/app/service/snapshot_recover.go @@ -0,0 +1,265 @@ +package service + +import ( + "context" + "fmt" + "os" + "path" + "strings" + "sync" + + "github.com/1Panel-dev/1Panel/agent/app/dto" + "github.com/1Panel-dev/1Panel/agent/app/model" + "github.com/1Panel-dev/1Panel/agent/constant" + "github.com/1Panel-dev/1Panel/agent/global" + "github.com/1Panel-dev/1Panel/agent/utils/cmd" + "github.com/1Panel-dev/1Panel/agent/utils/common" + "github.com/1Panel-dev/1Panel/agent/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 == "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, req.Secret); 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 + } + 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 + } + snapFileDir = fmt.Sprintf("%s/%s", baseDir, snap.Name) + if _, err := os.Stat(snapFileDir); err != nil { + snapFileDir = baseDir + } + } 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 isRecover && (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"); 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"); 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"); 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(4) + itemHelper.Wg = &wg + go snapJson(itemHelper, jsonItem, baseDir) + go snapPanel(itemHelper, path.Join(baseDir, "1panel")) + go snapDaemonJson(itemHelper, path.Join(baseDir, "docker")) + go snapBackup(itemHelper, global.CONF.System.Backup, path.Join(baseDir, "1panel")) + wg.Wait() + itemHelper.Status.AppData = constant.StatusDone + + 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) + } + if err := common.CopyFile(src, dst); err != nil { + return fmt.Errorf("cp file failed, err: %v", 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!") +} diff --git a/agent/app/service/ssh.go b/agent/app/service/ssh.go new file mode 100644 index 000000000..46e911119 --- /dev/null +++ b/agent/app/service/ssh.go @@ -0,0 +1,573 @@ +package service + +import ( + "fmt" + "os" + "os/user" + "path" + "path/filepath" + "sort" + "strings" + "time" + + "github.com/1Panel-dev/1Panel/agent/app/dto" + "github.com/1Panel-dev/1Panel/agent/buserr" + "github.com/1Panel-dev/1Panel/agent/constant" + "github.com/1Panel-dev/1Panel/agent/global" + "github.com/1Panel-dev/1Panel/agent/utils/cmd" + "github.com/1Panel-dev/1Panel/agent/utils/common" + "github.com/1Panel-dev/1Panel/agent/utils/files" + "github.com/1Panel-dev/1Panel/agent/utils/qqwry" + "github.com/1Panel-dev/1Panel/agent/utils/systemctl" + "github.com/pkg/errors" +) + +const sshPath = "/etc/ssh/sshd_config" + +type SSHService struct{} + +type ISSHService interface { + GetSSHInfo() (*dto.SSHInfo, error) + OperateSSH(operation string) error + UpdateByFile(value string) error + Update(req dto.SSHUpdate) error + GenerateSSH(req dto.GenerateSSH) error + LoadSSHSecret(mode string) (string, error) + LoadLog(req dto.SearchSSHLog) (*dto.SSHLog, error) + + LoadSSHConf() (string, error) +} + +func NewISSHService() ISSHService { + return &SSHService{} +} + +func (u *SSHService) GetSSHInfo() (*dto.SSHInfo, error) { + data := dto.SSHInfo{ + AutoStart: true, + Status: constant.StatusEnable, + Message: "", + Port: "22", + ListenAddress: "", + PasswordAuthentication: "yes", + PubkeyAuthentication: "yes", + PermitRootLogin: "yes", + UseDNS: "yes", + } + serviceName, err := loadServiceName() + if err != nil { + data.Status = constant.StatusDisable + data.Message = err.Error() + } else { + active, err := systemctl.IsActive(serviceName) + if !active { + data.Status = constant.StatusDisable + if err != nil { + data.Message = err.Error() + } + } else { + data.Status = constant.StatusEnable + } + } + + out, err := systemctl.RunSystemCtl("is-enabled", serviceName) + if err != nil { + data.AutoStart = false + } else { + if out == "alias\n" { + data.AutoStart, _ = systemctl.IsEnable("ssh") + } else { + data.AutoStart = out == "enabled\n" + } + } + + sshConf, err := os.ReadFile(sshPath) + if err != nil { + data.Message = err.Error() + data.Status = constant.StatusDisable + } + lines := strings.Split(string(sshConf), "\n") + for _, line := range lines { + if strings.HasPrefix(line, "Port ") { + data.Port = strings.ReplaceAll(line, "Port ", "") + } + if strings.HasPrefix(line, "ListenAddress ") { + itemAddr := strings.ReplaceAll(line, "ListenAddress ", "") + if len(data.ListenAddress) != 0 { + data.ListenAddress += ("," + itemAddr) + } else { + data.ListenAddress = itemAddr + } + } + if strings.HasPrefix(line, "PasswordAuthentication ") { + data.PasswordAuthentication = strings.ReplaceAll(line, "PasswordAuthentication ", "") + } + if strings.HasPrefix(line, "PubkeyAuthentication ") { + data.PubkeyAuthentication = strings.ReplaceAll(line, "PubkeyAuthentication ", "") + } + if strings.HasPrefix(line, "PermitRootLogin ") { + data.PermitRootLogin = strings.ReplaceAll(strings.ReplaceAll(line, "PermitRootLogin ", ""), "prohibit-password", "without-password") + } + if strings.HasPrefix(line, "UseDNS ") { + data.UseDNS = strings.ReplaceAll(line, "UseDNS ", "") + } + } + return &data, nil +} + +func (u *SSHService) OperateSSH(operation string) error { + serviceName, err := loadServiceName() + if err != nil { + return err + } + sudo := cmd.SudoHandleCmd() + if operation == "enable" || operation == "disable" { + serviceName += ".service" + } + stdout, err := cmd.Execf("%s systemctl %s %s", sudo, operation, serviceName) + if err != nil { + if strings.Contains(stdout, "alias name or linked unit file") { + stdout, err := cmd.Execf("%s systemctl %s ssh", sudo, operation) + if err != nil { + return fmt.Errorf("%s ssh(alias name or linked unit file) failed, stdout: %s, err: %v", operation, stdout, err) + } + } + return fmt.Errorf("%s %s failed, stdout: %s, err: %v", operation, serviceName, stdout, err) + } + return nil +} + +func (u *SSHService) Update(req dto.SSHUpdate) error { + serviceName, err := loadServiceName() + if err != nil { + return err + } + + sshConf, err := os.ReadFile(sshPath) + if err != nil { + return err + } + lines := strings.Split(string(sshConf), "\n") + newFiles := updateSSHConf(lines, req.Key, req.NewValue) + file, err := os.OpenFile(sshPath, os.O_WRONLY|os.O_TRUNC, 0666) + if err != nil { + return err + } + defer file.Close() + if _, err = file.WriteString(strings.Join(newFiles, "\n")); err != nil { + return err + } + sudo := cmd.SudoHandleCmd() + if req.Key == "Port" { + stdout, _ := cmd.Execf("%s getenforce", sudo) + if stdout == "Enforcing\n" { + _, _ = cmd.Execf("%s semanage port -a -t ssh_port_t -p tcp %s", sudo, req.NewValue) + } + + ruleItem := dto.PortRuleUpdate{ + OldRule: dto.PortRuleOperate{ + Operation: "remove", + Port: req.OldValue, + Protocol: "tcp", + Strategy: "accept", + }, + NewRule: dto.PortRuleOperate{ + Operation: "add", + Port: req.NewValue, + Protocol: "tcp", + Strategy: "accept", + }, + } + if err := NewIFirewallService().UpdatePortRule(ruleItem); err != nil { + 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) + } + } + + _, _ = cmd.Execf("%s systemctl restart %s", sudo, serviceName) + return nil +} + +func (u *SSHService) UpdateByFile(value string) error { + serviceName, err := loadServiceName() + if err != nil { + return err + } + + file, err := os.OpenFile(sshPath, os.O_WRONLY|os.O_TRUNC, 0666) + if err != nil { + return err + } + defer file.Close() + if _, err = file.WriteString(value); err != nil { + return err + } + sudo := cmd.SudoHandleCmd() + _, _ = cmd.Execf("%s systemctl restart %s", sudo, serviceName) + return nil +} + +func (u *SSHService) GenerateSSH(req dto.GenerateSSH) error { + if cmd.CheckIllegal(req.EncryptionMode, req.Password) { + return buserr.New(constant.ErrCmdIllegal) + } + currentUser, err := user.Current() + if err != nil { + return fmt.Errorf("load current user failed, err: %v", err) + } + secretFile := fmt.Sprintf("%s/.ssh/id_item_%s", currentUser.HomeDir, req.EncryptionMode) + secretPubFile := fmt.Sprintf("%s/.ssh/id_item_%s.pub", currentUser.HomeDir, req.EncryptionMode) + authFilePath := currentUser.HomeDir + "/.ssh/authorized_keys" + + command := fmt.Sprintf("ssh-keygen -t %s -f %s/.ssh/id_item_%s | echo y", req.EncryptionMode, currentUser.HomeDir, req.EncryptionMode) + if len(req.Password) != 0 { + command = fmt.Sprintf("ssh-keygen -t %s -P %s -f %s/.ssh/id_item_%s | echo y", req.EncryptionMode, req.Password, currentUser.HomeDir, req.EncryptionMode) + } + stdout, err := cmd.Exec(command) + if err != nil { + return fmt.Errorf("generate failed, err: %v, message: %s", err, stdout) + } + defer func() { + _ = os.Remove(secretFile) + }() + defer func() { + _ = os.Remove(secretPubFile) + }() + + if _, err := os.Stat(authFilePath); err != nil && errors.Is(err, os.ErrNotExist) { + authFile, err := os.Create(authFilePath) + if err != nil { + return err + } + defer authFile.Close() + } + stdout1, err := cmd.Execf("cat %s >> %s/.ssh/authorized_keys", secretPubFile, currentUser.HomeDir) + if err != nil { + return fmt.Errorf("generate failed, err: %v, message: %s", err, stdout1) + } + + fileOp := files.NewFileOp() + if err := fileOp.Rename(secretFile, fmt.Sprintf("%s/.ssh/id_%s", currentUser.HomeDir, req.EncryptionMode)); err != nil { + return err + } + if err := fileOp.Rename(secretPubFile, fmt.Sprintf("%s/.ssh/id_%s.pub", currentUser.HomeDir, req.EncryptionMode)); err != nil { + return err + } + + return nil +} + +func (u *SSHService) LoadSSHSecret(mode string) (string, error) { + currentUser, err := user.Current() + if err != nil { + return "", fmt.Errorf("load current user failed, err: %v", err) + } + + homeDir := currentUser.HomeDir + if _, err := os.Stat(fmt.Sprintf("%s/.ssh/id_%s", homeDir, mode)); err != nil { + return "", nil + } + file, err := os.ReadFile(fmt.Sprintf("%s/.ssh/id_%s", homeDir, mode)) + return string(file), err +} + +type sshFileItem struct { + Name string + Year int +} + +func (u *SSHService) LoadLog(req dto.SearchSSHLog) (*dto.SSHLog, error) { + var fileList []sshFileItem + var data dto.SSHLog + baseDir := "/var/log" + if err := filepath.Walk(baseDir, func(pathItem string, info os.FileInfo, err error) error { + if err != nil { + return err + } + if !info.IsDir() && (strings.HasPrefix(info.Name(), "secure") || strings.HasPrefix(info.Name(), "auth")) { + if !strings.HasSuffix(info.Name(), ".gz") { + fileList = append(fileList, sshFileItem{Name: pathItem, Year: info.ModTime().Year()}) + return nil + } + itemFileName := strings.TrimSuffix(pathItem, ".gz") + if _, err := os.Stat(itemFileName); err != nil && os.IsNotExist(err) { + if err := handleGunzip(pathItem); err == nil { + fileList = append(fileList, sshFileItem{Name: itemFileName, Year: info.ModTime().Year()}) + } + } + } + return nil + }); err != nil { + return nil, err + } + fileList = sortFileList(fileList) + + command := "" + if len(req.Info) != 0 { + command = fmt.Sprintf(" | grep '%s'", req.Info) + } + + showCountFrom := (req.Page - 1) * req.PageSize + showCountTo := req.Page * req.PageSize + nyc, _ := time.LoadLocation(common.LoadTimeZone()) + qqWry, err := qqwry.NewQQwry() + if err != nil { + global.LOG.Errorf("load qqwry datas failed: %s", err) + } + for _, file := range fileList { + commandItem := "" + if strings.HasPrefix(path.Base(file.Name), "secure") { + switch req.Status { + case constant.StatusSuccess: + commandItem = fmt.Sprintf("cat %s | grep -a Accepted %s", file.Name, command) + case constant.StatusFailed: + commandItem = fmt.Sprintf("cat %s | grep -a 'Failed password for' %s", file.Name, command) + default: + commandItem = fmt.Sprintf("cat %s | grep -aE '(Failed password for|Accepted)' %s", file.Name, command) + } + } + if strings.HasPrefix(path.Base(file.Name), "auth.log") { + switch req.Status { + case constant.StatusSuccess: + commandItem = fmt.Sprintf("cat %s | grep -a Accepted %s", file.Name, command) + case constant.StatusFailed: + commandItem = fmt.Sprintf("cat %s | grep -aE 'Failed password for|Connection closed by authenticating user' %s", file.Name, command) + default: + commandItem = fmt.Sprintf("cat %s | grep -aE \"(Failed password for|Connection closed by authenticating user|Accepted)\" %s", file.Name, command) + } + } + dataItem, successCount, failedCount := loadSSHData(commandItem, showCountFrom, showCountTo, file.Year, qqWry, nyc) + data.FailedCount += failedCount + data.TotalCount += successCount + failedCount + showCountFrom = showCountFrom - (successCount + failedCount) + showCountTo = showCountTo - (successCount + failedCount) + data.Logs = append(data.Logs, dataItem...) + } + + data.SuccessfulCount = data.TotalCount - data.FailedCount + return &data, nil +} + +func (u *SSHService) LoadSSHConf() (string, error) { + if _, err := os.Stat("/etc/ssh/sshd_config"); err != nil { + return "", buserr.New("ErrHttpReqNotFound") + } + content, err := os.ReadFile("/etc/ssh/sshd_config") + if err != nil { + return "", err + } + return string(content), nil +} + +func sortFileList(fileNames []sshFileItem) []sshFileItem { + if len(fileNames) < 2 { + return fileNames + } + if strings.HasPrefix(path.Base(fileNames[0].Name), "secure") { + var itemFile []sshFileItem + sort.Slice(fileNames, func(i, j int) bool { + return fileNames[i].Name > fileNames[j].Name + }) + itemFile = append(itemFile, fileNames[len(fileNames)-1]) + itemFile = append(itemFile, fileNames[:len(fileNames)-1]...) + return itemFile + } + sort.Slice(fileNames, func(i, j int) bool { + return fileNames[i].Name < fileNames[j].Name + }) + return fileNames +} + +func updateSSHConf(oldFiles []string, param string, value string) []string { + var valueItems []string + if param != "ListenAddress" { + valueItems = append(valueItems, value) + } else { + if value != "" { + valueItems = strings.Split(value, ",") + } + } + var newFiles []string + for _, line := range oldFiles { + lineItem := strings.TrimSpace(line) + if (strings.HasPrefix(lineItem, param) || strings.HasPrefix(lineItem, fmt.Sprintf("#%s", param))) && len(valueItems) != 0 { + newFiles = append(newFiles, fmt.Sprintf("%s %s", param, valueItems[0])) + valueItems = valueItems[1:] + continue + } + if strings.HasPrefix(lineItem, param) && len(valueItems) == 0 { + newFiles = append(newFiles, fmt.Sprintf("#%s", line)) + continue + } + newFiles = append(newFiles, line) + } + if len(valueItems) != 0 { + for _, item := range valueItems { + newFiles = append(newFiles, fmt.Sprintf("%s %s", param, item)) + } + } + return newFiles +} + +func loadSSHData(command string, showCountFrom, showCountTo, currentYear int, qqWry *qqwry.QQwry, nyc *time.Location) ([]dto.SSHHistory, int, int) { + var ( + datas []dto.SSHHistory + successCount int + failedCount int + ) + stdout2, err := cmd.Exec(command) + if err != nil { + return datas, 0, 0 + } + lines := strings.Split(string(stdout2), "\n") + for i := len(lines) - 1; i >= 0; i-- { + var itemData dto.SSHHistory + switch { + case strings.Contains(lines[i], "Failed password for"): + itemData = loadFailedSecureDatas(lines[i]) + if len(itemData.Address) != 0 { + if successCount+failedCount >= showCountFrom && successCount+failedCount < showCountTo { + itemData.Area = qqWry.Find(itemData.Address).Area + itemData.Date = loadDate(currentYear, itemData.DateStr, nyc) + datas = append(datas, itemData) + } + failedCount++ + } + case strings.Contains(lines[i], "Connection closed by authenticating user"): + itemData = loadFailedAuthDatas(lines[i]) + if len(itemData.Address) != 0 { + if successCount+failedCount >= showCountFrom && successCount+failedCount < showCountTo { + itemData.Area = qqWry.Find(itemData.Address).Area + itemData.Date = loadDate(currentYear, itemData.DateStr, nyc) + datas = append(datas, itemData) + } + failedCount++ + } + case strings.Contains(lines[i], "Accepted "): + itemData = loadSuccessDatas(lines[i]) + if len(itemData.Address) != 0 { + if successCount+failedCount >= showCountFrom && successCount+failedCount < showCountTo { + itemData.Area = qqWry.Find(itemData.Address).Area + itemData.Date = loadDate(currentYear, itemData.DateStr, nyc) + datas = append(datas, itemData) + } + successCount++ + } + } + } + return datas, successCount, failedCount +} + +func loadSuccessDatas(line string) dto.SSHHistory { + var data dto.SSHHistory + parts := strings.Fields(line) + index, dataStr := analyzeDateStr(parts) + if dataStr == "" { + return data + } + data.DateStr = dataStr + data.AuthMode = parts[4+index] + data.User = parts[6+index] + data.Address = parts[8+index] + data.Port = parts[10+index] + data.Status = constant.StatusSuccess + return data +} + +func loadFailedAuthDatas(line string) dto.SSHHistory { + var data dto.SSHHistory + parts := strings.Fields(line) + index, dataStr := analyzeDateStr(parts) + if dataStr == "" { + return data + } + data.DateStr = dataStr + switch index { + case 1: + data.User = parts[9] + case 2: + data.User = parts[10] + default: + data.User = parts[7] + } + data.AuthMode = parts[6+index] + data.Address = parts[9+index] + data.Port = parts[11+index] + data.Status = constant.StatusFailed + if strings.Contains(line, ": ") { + data.Message = strings.Split(line, ": ")[1] + } + return data +} +func loadFailedSecureDatas(line string) dto.SSHHistory { + var data dto.SSHHistory + parts := strings.Fields(line) + index, dataStr := analyzeDateStr(parts) + if dataStr == "" { + return data + } + data.DateStr = dataStr + if strings.Contains(line, " invalid ") { + data.AuthMode = parts[4+index] + index += 2 + } else { + data.AuthMode = parts[4+index] + } + data.User = parts[6+index] + data.Address = parts[8+index] + data.Port = parts[10+index] + data.Status = constant.StatusFailed + if strings.Contains(line, ": ") { + data.Message = strings.Split(line, ": ")[1] + } + return data +} + +func handleGunzip(path string) error { + if _, err := cmd.Execf("gunzip %s", path); err != nil { + return err + } + return nil +} + +func loadServiceName() (string, error) { + if exist, _ := systemctl.IsExist("sshd"); exist { + return "sshd", nil + } else if exist, _ := systemctl.IsExist("ssh"); exist { + return "ssh", nil + } + return "", errors.New("The ssh or sshd service is unavailable") +} + +func loadDate(currentYear int, DateStr string, nyc *time.Location) time.Time { + itemDate, err := time.ParseInLocation("2006 Jan 2 15:04:05", fmt.Sprintf("%d %s", currentYear, DateStr), nyc) + if err != nil { + itemDate, _ = time.ParseInLocation("2006 Jan 2 15:04:05", DateStr, nyc) + } + return itemDate +} + +func analyzeDateStr(parts []string) (int, string) { + t, err := time.Parse(time.RFC3339Nano, parts[0]) + if err == nil { + if len(parts) < 12 { + return 0, "" + } + return 0, t.Format("2006 Jan 2 15:04:05") + } + t, err = time.Parse(constant.DateTimeLayout, fmt.Sprintf("%s %s", parts[0], parts[1])) + if err == nil { + if len(parts) < 14 { + return 0, "" + } + return 1, t.Format("2006 Jan 2 15:04:05") + } + + if len(parts) < 14 { + return 0, "" + } + return 2, fmt.Sprintf("%s %s %s", parts[0], parts[1], parts[2]) +} diff --git a/agent/app/service/website.go b/agent/app/service/website.go new file mode 100644 index 000000000..debcc3664 --- /dev/null +++ b/agent/app/service/website.go @@ -0,0 +1,2565 @@ +package service + +import ( + "bufio" + "bytes" + "context" + "crypto/x509" + "encoding/json" + "encoding/pem" + "errors" + "fmt" + "os" + "path" + "reflect" + "regexp" + "strconv" + "strings" + "syscall" + "time" + + "github.com/1Panel-dev/1Panel/agent/utils/common" + "github.com/jinzhu/copier" + + "github.com/1Panel-dev/1Panel/agent/i18n" + "github.com/spf13/afero" + + "github.com/1Panel-dev/1Panel/agent/utils/compose" + "github.com/1Panel-dev/1Panel/agent/utils/env" + + "github.com/1Panel-dev/1Panel/agent/app/api/v1/helper" + "github.com/1Panel-dev/1Panel/agent/cmd/server/nginx_conf" + "github.com/1Panel-dev/1Panel/agent/utils/cmd" + "github.com/1Panel-dev/1Panel/agent/utils/nginx" + "github.com/1Panel-dev/1Panel/agent/utils/nginx/components" + "github.com/1Panel-dev/1Panel/agent/utils/nginx/parser" + "golang.org/x/crypto/bcrypt" + "gopkg.in/ini.v1" + "gorm.io/gorm" + + "github.com/1Panel-dev/1Panel/agent/app/dto/request" + "github.com/1Panel-dev/1Panel/agent/app/dto/response" + "github.com/1Panel-dev/1Panel/agent/app/repo" + "github.com/1Panel-dev/1Panel/agent/buserr" + "github.com/1Panel-dev/1Panel/agent/global" + + "github.com/1Panel-dev/1Panel/agent/app/dto" + "github.com/1Panel-dev/1Panel/agent/app/model" + "github.com/1Panel-dev/1Panel/agent/constant" + "github.com/1Panel-dev/1Panel/agent/utils/files" +) + +type WebsiteService struct { +} + +type IWebsiteService interface { + PageWebsite(req request.WebsiteSearch) (int64, []response.WebsiteRes, error) + GetWebsites() ([]response.WebsiteDTO, error) + CreateWebsite(create request.WebsiteCreate) error + OpWebsite(req request.WebsiteOp) error + GetWebsiteOptions() ([]response.WebsiteOption, error) + UpdateWebsite(req request.WebsiteUpdate) error + DeleteWebsite(req request.WebsiteDelete) error + GetWebsite(id uint) (response.WebsiteDTO, error) + + CreateWebsiteDomain(create request.WebsiteDomainCreate) ([]model.WebsiteDomain, error) + GetWebsiteDomain(websiteId uint) ([]model.WebsiteDomain, error) + DeleteWebsiteDomain(domainId uint) error + + GetNginxConfigByScope(req request.NginxScopeReq) (*response.WebsiteNginxConfig, error) + UpdateNginxConfigByScope(req request.NginxConfigUpdate) error + GetWebsiteNginxConfig(websiteId uint, configType string) (response.FileInfo, error) + UpdateNginxConfigFile(req request.WebsiteNginxUpdate) error + GetWebsiteHTTPS(websiteId uint) (response.WebsiteHTTPS, error) + OpWebsiteHTTPS(ctx context.Context, req request.WebsiteHTTPSOp) (*response.WebsiteHTTPS, error) + OpWebsiteLog(req request.WebsiteLogReq) (*response.WebsiteLog, error) + ChangeDefaultServer(id uint) error + PreInstallCheck(req request.WebsiteInstallCheckReq) ([]response.WebsitePreInstallCheck, error) + + GetPHPConfig(id uint) (*response.PHPConfig, error) + UpdatePHPConfig(req request.WebsitePHPConfigUpdate) error + UpdatePHPConfigFile(req request.WebsitePHPFileUpdate) error + ChangePHPVersion(req request.WebsitePHPVersionReq) error + + GetRewriteConfig(req request.NginxRewriteReq) (*response.NginxRewriteRes, error) + UpdateRewriteConfig(req request.NginxRewriteUpdate) error + LoadWebsiteDirConfig(req request.WebsiteCommonReq) (*response.WebsiteDirConfig, error) + UpdateSiteDir(req request.WebsiteUpdateDir) error + UpdateSitePermission(req request.WebsiteUpdateDirPermission) error + OperateProxy(req request.WebsiteProxyConfig) (err error) + GetProxies(id uint) (res []request.WebsiteProxyConfig, err error) + UpdateProxyFile(req request.NginxProxyUpdate) (err error) + GetAuthBasics(req request.NginxAuthReq) (res response.NginxAuthRes, err error) + UpdateAuthBasic(req request.NginxAuthUpdate) (err error) + GetAntiLeech(id uint) (*response.NginxAntiLeechRes, error) + UpdateAntiLeech(req request.NginxAntiLeechUpdate) (err error) + OperateRedirect(req request.NginxRedirectReq) (err error) + GetRedirect(id uint) (res []response.NginxRedirectConfig, err error) + UpdateRedirectFile(req request.NginxRedirectUpdate) (err error) + + UpdateDefaultHtml(req request.WebsiteHtmlUpdate) error + GetDefaultHtml(resourceType string) (*response.WebsiteHtmlRes, error) +} + +func NewIWebsiteService() IWebsiteService { + return &WebsiteService{} +} + +func (w WebsiteService) PageWebsite(req request.WebsiteSearch) (int64, []response.WebsiteRes, error) { + var ( + websiteDTOs []response.WebsiteRes + opts []repo.DBOption + ) + nginxInstall, err := getAppInstallByKey(constant.AppOpenresty) + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return 0, nil, nil + } + return 0, nil, err + } + opts = append(opts, commonRepo.WithOrderRuleBy(req.OrderBy, req.Order)) + if 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)) + } + total, websites, err := websiteRepo.Page(req.Page, req.PageSize, opts...) + if err != nil { + return 0, nil, err + } + for _, web := range websites { + var ( + appName string + runtimeName string + ) + switch web.Type { + case constant.Deployment: + appInstall, err := appInstallRepo.GetFirst(commonRepo.WithByID(web.AppInstallID)) + if err != nil { + return 0, nil, err + } + appName = appInstall.Name + case constant.Runtime: + runtime, err := runtimeRepo.GetFirst(commonRepo.WithByID(web.RuntimeID)) + if err != nil { + return 0, nil, err + } + runtimeName = runtime.Name + } + sitePath := path.Join(constant.AppInstallDir, constant.AppOpenresty, nginxInstall.Name, "www", "sites", web.Alias) + + websiteDTOs = append(websiteDTOs, response.WebsiteRes{ + ID: web.ID, + CreatedAt: web.CreatedAt, + Protocol: web.Protocol, + PrimaryDomain: web.PrimaryDomain, + Type: web.Type, + Remark: web.Remark, + Status: web.Status, + Alias: web.Alias, + AppName: appName, + ExpireDate: web.ExpireDate, + SSLExpireDate: web.WebsiteSSL.ExpireDate, + SSLStatus: checkSSLStatus(web.WebsiteSSL.ExpireDate), + RuntimeName: runtimeName, + SitePath: sitePath, + }) + } + return total, websiteDTOs, nil +} + +func (w WebsiteService) GetWebsites() ([]response.WebsiteDTO, error) { + var websiteDTOs []response.WebsiteDTO + websites, err := websiteRepo.List(commonRepo.WithOrderRuleBy("primary_domain", "ascending")) + if err != nil { + return nil, err + } + for _, web := range websites { + websiteDTOs = append(websiteDTOs, response.WebsiteDTO{ + Website: web, + }) + } + return websiteDTOs, nil +} + +func (w WebsiteService) CreateWebsite(create request.WebsiteCreate) (err error) { + alias := create.Alias + if alias == "default" { + return buserr.New("ErrDefaultAlias") + } + if common.ContainsChinese(alias) { + alias, err = common.PunycodeEncode(alias) + if err != nil { + return + } + } + if exist, _ := websiteRepo.GetBy(websiteRepo.WithAlias(alias)); len(exist) > 0 { + return buserr.New(constant.ErrAliasIsExist) + } + + nginxInstall, err := getAppInstallByKey(constant.AppOpenresty) + if err != nil { + return err + } + defaultHttpPort := nginxInstall.HttpPort + + var ( + otherDomains []model.WebsiteDomain + domains []model.WebsiteDomain + ) + domains, _, _, err = getWebsiteDomains(create.PrimaryDomain, defaultHttpPort, 0) + if err != nil { + return err + } + otherDomains, _, _, err = getWebsiteDomains(create.OtherDomains, defaultHttpPort, 0) + if err != nil { + return err + } + domains = append(domains, otherDomains...) + + defaultDate, _ := time.Parse(constant.DateLayout, constant.DefaultDate) + website := &model.Website{ + PrimaryDomain: create.PrimaryDomain, + Type: create.Type, + Alias: alias, + Remark: create.Remark, + Status: constant.WebRunning, + ExpireDate: defaultDate, + WebsiteGroupID: create.WebsiteGroupID, + Protocol: constant.ProtocolHTTP, + Proxy: create.Proxy, + SiteDir: "/", + AccessLog: true, + ErrorLog: true, + IPV6: create.IPV6, + } + + var ( + appInstall *model.AppInstall + runtime *model.Runtime + ) + + defer func() { + if err != nil { + if website.AppInstallID > 0 { + req := request.AppInstalledOperate{ + InstallId: website.AppInstallID, + Operate: constant.Delete, + ForceDelete: true, + } + if err := NewIAppInstalledService().Operate(req); err != nil { + global.LOG.Errorf(err.Error()) + } + } + } + }() + var proxy string + + switch create.Type { + case constant.Deployment: + if create.AppType == constant.NewApp { + var ( + req request.AppInstallCreate + install *model.AppInstall + ) + req.Name = create.AppInstall.Name + req.AppDetailId = create.AppInstall.AppDetailId + req.Params = create.AppInstall.Params + req.AppContainerConfig = create.AppInstall.AppContainerConfig + tx, installCtx := getTxAndContext() + install, err = NewIAppService().Install(installCtx, req) + if err != nil { + tx.Rollback() + return err + } + tx.Commit() + appInstall = install + website.AppInstallID = install.ID + website.Proxy = fmt.Sprintf("127.0.0.1:%d", appInstall.HttpPort) + } else { + var install model.AppInstall + install, err = appInstallRepo.GetFirst(commonRepo.WithByID(create.AppInstallID)) + if err != nil { + return err + } + appInstall = &install + website.AppInstallID = appInstall.ID + website.Proxy = fmt.Sprintf("127.0.0.1:%d", appInstall.HttpPort) + } + case constant.Runtime: + runtime, err = runtimeRepo.GetFirst(commonRepo.WithByID(create.RuntimeID)) + if err != nil { + return err + } + website.RuntimeID = runtime.ID + switch runtime.Type { + case constant.RuntimePHP: + if runtime.Resource == constant.ResourceAppstore { + var ( + req request.AppInstallCreate + install *model.AppInstall + ) + reg, _ := regexp.Compile(`[^a-z0-9_-]+`) + req.Name = reg.ReplaceAllString(strings.ToLower(alias), "") + req.AppDetailId = create.AppInstall.AppDetailId + req.Params = create.AppInstall.Params + req.Params["IMAGE_NAME"] = runtime.Image + req.AppContainerConfig = create.AppInstall.AppContainerConfig + req.Params["PANEL_WEBSITE_DIR"] = path.Join(nginxInstall.GetPath(), "/www") + tx, installCtx := getTxAndContext() + install, err = NewIAppService().Install(installCtx, req) + if err != nil { + tx.Rollback() + return err + } + tx.Commit() + website.AppInstallID = install.ID + appInstall = install + website.Proxy = fmt.Sprintf("127.0.0.1:%d", appInstall.HttpPort) + } else { + website.ProxyType = create.ProxyType + if website.ProxyType == constant.RuntimeProxyUnix { + proxy = fmt.Sprintf("unix:%s", path.Join("/www/sites", website.Alias, "php-pool", "php-fpm.sock")) + } + if website.ProxyType == constant.RuntimeProxyTcp { + proxy = fmt.Sprintf("127.0.0.1:%d", create.Port) + } + website.Proxy = proxy + } + case constant.RuntimeNode, constant.RuntimeJava, constant.RuntimeGo: + website.Proxy = fmt.Sprintf("127.0.0.1:%d", runtime.Port) + } + } + + if err = configDefaultNginx(website, domains, appInstall, runtime); err != nil { + return err + } + + if len(create.FtpUser) != 0 && len(create.FtpPassword) != 0 { + indexDir := path.Join(nginxInstall.GetPath(), "www", "sites", website.Alias, "index") + itemID, err := NewIFtpService().Create(dto.FtpCreate{User: create.FtpUser, Password: create.FtpPassword, Path: indexDir}) + if err != nil { + global.LOG.Errorf("create ftp for website failed, err: %v", err) + } + website.FtpID = itemID + } + + if err = createWafConfig(website, domains); err != nil { + return err + } + + tx, ctx := helper.GetTxAndContext() + defer tx.Rollback() + if err = websiteRepo.Create(ctx, website); err != nil { + return err + } + for i := range domains { + domains[i].WebsiteID = website.ID + } + if err = websiteDomainRepo.BatchCreate(ctx, domains); err != nil { + return err + } + tx.Commit() + return nil +} + +func (w WebsiteService) OpWebsite(req request.WebsiteOp) error { + website, err := websiteRepo.GetFirst(commonRepo.WithByID(req.ID)) + if err != nil { + return err + } + if err := opWebsite(&website, req.Operate); err != nil { + return err + } + return websiteRepo.Save(context.Background(), &website) +} + +func (w WebsiteService) GetWebsiteOptions() ([]response.WebsiteOption, error) { + webs, err := websiteRepo.List() + if err != nil { + return nil, err + } + var datas []response.WebsiteOption + for _, web := range webs { + var item response.WebsiteOption + if err := copier.Copy(&item, &web); err != nil { + return nil, err + } + datas = append(datas, item) + } + return datas, nil +} + +func (w WebsiteService) UpdateWebsite(req request.WebsiteUpdate) error { + website, err := websiteRepo.GetFirst(commonRepo.WithByID(req.ID)) + if err != nil { + return err + } + if website.IPV6 != req.IPV6 { + if err := changeIPV6(website, req.IPV6); err != nil { + return err + } + } + website.PrimaryDomain = req.PrimaryDomain + website.WebsiteGroupID = req.WebsiteGroupID + website.Remark = req.Remark + website.IPV6 = req.IPV6 + + if req.ExpireDate != "" { + expireDate, err := time.Parse(constant.DateLayout, req.ExpireDate) + if err != nil { + return err + } + website.ExpireDate = expireDate + } + + return websiteRepo.Save(context.TODO(), &website) +} + +func (w WebsiteService) GetWebsite(id uint) (response.WebsiteDTO, error) { + var res response.WebsiteDTO + website, err := websiteRepo.GetFirst(commonRepo.WithByID(id)) + if err != nil { + return res, err + } + res.Website = website + + nginxInstall, err := getAppInstallByKey(constant.AppOpenresty) + if err != nil { + return res, err + } + sitePath := path.Join(constant.AppInstallDir, constant.AppOpenresty, nginxInstall.Name, "www", "sites", website.Alias) + res.ErrorLogPath = path.Join(sitePath, "log", "error.log") + res.AccessLogPath = path.Join(sitePath, "log", "access.log") + res.SitePath = sitePath + res.SiteDir = website.SiteDir + return res, nil +} + +func (w WebsiteService) DeleteWebsite(req request.WebsiteDelete) error { + website, err := websiteRepo.GetFirst(commonRepo.WithByID(req.ID)) + if err != nil { + return err + } + if err = delNginxConfig(website, req.ForceDelete); err != nil { + return err + } + + if err = delWafConfig(website, req.ForceDelete); err != nil { + return err + } + + if checkIsLinkApp(website) && req.DeleteApp { + appInstall, _ := appInstallRepo.GetFirst(commonRepo.WithByID(website.AppInstallID)) + if appInstall.ID > 0 { + if err = deleteAppInstall(appInstall, true, req.ForceDelete, true); err != nil && !req.ForceDelete { + return err + } + } + } + + tx, ctx := helper.GetTxAndContext() + defer tx.Rollback() + + go func() { + _ = NewIBackupService().DeleteRecordByName("website", website.PrimaryDomain, website.Alias, req.DeleteBackup) + }() + + if err := websiteRepo.DeleteBy(ctx, commonRepo.WithByID(req.ID)); err != nil { + return err + } + if err := websiteDomainRepo.DeleteBy(ctx, websiteDomainRepo.WithWebsiteId(req.ID)); err != nil { + return err + } + tx.Commit() + + uploadDir := path.Join(global.CONF.System.BaseDir, fmt.Sprintf("1panel/uploads/website/%s", website.Alias)) + if _, err := os.Stat(uploadDir); err == nil { + _ = os.RemoveAll(uploadDir) + } + return nil +} + +func (w WebsiteService) CreateWebsiteDomain(create request.WebsiteDomainCreate) ([]model.WebsiteDomain, error) { + var ( + domainModels []model.WebsiteDomain + addPorts []int + addDomains []string + ) + httpPort, _, err := getAppInstallPort(constant.AppOpenresty) + if err != nil { + return nil, err + } + website, err := websiteRepo.GetFirst(commonRepo.WithByID(create.WebsiteID)) + if err != nil { + return nil, err + } + + domainModels, addPorts, addDomains, err = getWebsiteDomains(create.Domains, httpPort, create.WebsiteID) + if err != nil { + return nil, err + } + go func() { + _ = OperateFirewallPort(nil, addPorts) + }() + + if err := addListenAndServerName(website, addPorts, addDomains); err != nil { + return nil, err + } + + nginxInstall, err := getAppInstallByKey(constant.AppOpenresty) + if err != nil { + return nil, err + } + wafDataPath := path.Join(nginxInstall.GetPath(), "1pwaf", "data") + fileOp := files.NewFileOp() + if fileOp.Stat(wafDataPath) { + websitesConfigPath := path.Join(wafDataPath, "conf", "sites.json") + content, err := fileOp.GetContent(websitesConfigPath) + if err != nil { + return nil, err + } + var websitesArray []request.WafWebsite + if content != nil { + if err := json.Unmarshal(content, &websitesArray); err != nil { + return nil, err + } + } + for index, wafWebsite := range websitesArray { + if wafWebsite.Key == website.Alias { + wafSite := request.WafWebsite{ + Key: website.Alias, + Domains: wafWebsite.Domains, + Host: wafWebsite.Host, + } + for _, domain := range domainModels { + wafSite.Domains = append(wafSite.Domains, domain.Domain) + wafSite.Host = append(wafSite.Host, domain.Domain+":"+strconv.Itoa(domain.Port)) + } + if len(wafSite.Host) == 0 { + wafSite.Host = []string{} + } + websitesArray[index] = wafSite + break + } + } + websitesContent, err := json.Marshal(websitesArray) + if err != nil { + return nil, err + } + if err := fileOp.SaveFileWithByte(websitesConfigPath, websitesContent, 0644); err != nil { + return nil, err + } + } + + return domainModels, websiteDomainRepo.BatchCreate(context.TODO(), domainModels) +} + +func (w WebsiteService) GetWebsiteDomain(websiteId uint) ([]model.WebsiteDomain, error) { + return websiteDomainRepo.GetBy(websiteDomainRepo.WithWebsiteId(websiteId)) +} + +func (w WebsiteService) DeleteWebsiteDomain(domainId uint) error { + webSiteDomain, err := websiteDomainRepo.GetFirst(commonRepo.WithByID(domainId)) + if err != nil { + return err + } + + if websiteDomains, _ := websiteDomainRepo.GetBy(websiteDomainRepo.WithWebsiteId(webSiteDomain.WebsiteID)); len(websiteDomains) == 1 { + return fmt.Errorf("can not delete last domain") + } + website, err := websiteRepo.GetFirst(commonRepo.WithByID(webSiteDomain.WebsiteID)) + if err != nil { + return err + } + var ports []int + if oldDomains, _ := websiteDomainRepo.GetBy(websiteDomainRepo.WithWebsiteId(webSiteDomain.WebsiteID), websiteDomainRepo.WithPort(webSiteDomain.Port)); len(oldDomains) == 1 { + ports = append(ports, webSiteDomain.Port) + } + + var domains []string + if oldDomains, _ := websiteDomainRepo.GetBy(websiteDomainRepo.WithWebsiteId(webSiteDomain.WebsiteID), websiteDomainRepo.WithDomain(webSiteDomain.Domain)); len(oldDomains) == 1 { + domains = append(domains, webSiteDomain.Domain) + } + + if len(ports) > 0 || len(domains) > 0 { + stringBinds := make([]string, len(ports)) + for i := 0; i < len(ports); i++ { + stringBinds[i] = strconv.Itoa(ports[i]) + } + if err := deleteListenAndServerName(website, stringBinds, domains); err != nil { + return err + } + } + + nginxInstall, err := getAppInstallByKey(constant.AppOpenresty) + if err != nil { + return err + } + wafDataPath := path.Join(nginxInstall.GetPath(), "1pwaf", "data") + fileOp := files.NewFileOp() + if fileOp.Stat(wafDataPath) { + websitesConfigPath := path.Join(wafDataPath, "conf", "sites.json") + content, err := fileOp.GetContent(websitesConfigPath) + if err != nil { + return err + } + var websitesArray []request.WafWebsite + var newWebsitesArray []request.WafWebsite + if content != nil { + if err := json.Unmarshal(content, &websitesArray); err != nil { + return err + } + } + for _, wafWebsite := range websitesArray { + if wafWebsite.Key == website.Alias { + wafSite := wafWebsite + oldDomains := wafSite.Domains + var newDomains []string + for _, domain := range oldDomains { + if domain == webSiteDomain.Domain { + continue + } + newDomains = append(newDomains, domain) + } + wafSite.Domains = newDomains + oldHostArray := wafSite.Host + var newHostArray []string + for _, host := range oldHostArray { + if host == webSiteDomain.Domain+":"+strconv.Itoa(webSiteDomain.Port) { + continue + } + newHostArray = append(newHostArray, host) + } + wafSite.Host = newHostArray + if len(wafSite.Host) == 0 { + wafSite.Host = []string{} + } + newWebsitesArray = append(newWebsitesArray, wafSite) + } else { + newWebsitesArray = append(newWebsitesArray, wafWebsite) + } + } + websitesContent, err := json.Marshal(newWebsitesArray) + if err != nil { + return err + } + if err = fileOp.SaveFileWithByte(websitesConfigPath, websitesContent, 0644); err != nil { + return err + } + } + + return websiteDomainRepo.DeleteBy(context.TODO(), commonRepo.WithByID(domainId)) +} + +func (w WebsiteService) GetNginxConfigByScope(req request.NginxScopeReq) (*response.WebsiteNginxConfig, error) { + keys, ok := dto.ScopeKeyMap[req.Scope] + if !ok || len(keys) == 0 { + return nil, nil + } + + website, err := websiteRepo.GetFirst(commonRepo.WithByID(req.WebsiteID)) + if err != nil { + return nil, err + } + var config response.WebsiteNginxConfig + params, err := getNginxParamsByKeys(constant.NginxScopeServer, keys, &website) + if err != nil { + return nil, err + } + config.Params = params + config.Enable = len(params[0].Params) > 0 + + return &config, nil +} + +func (w WebsiteService) UpdateNginxConfigByScope(req request.NginxConfigUpdate) error { + keys, ok := dto.ScopeKeyMap[req.Scope] + if !ok || len(keys) == 0 { + return nil + } + website, err := websiteRepo.GetFirst(commonRepo.WithByID(req.WebsiteID)) + if err != nil { + return err + } + if req.Operate == constant.ConfigDel { + var nginxParams []dto.NginxParam + for _, key := range keys { + nginxParams = append(nginxParams, dto.NginxParam{ + Name: key, + }) + } + return deleteNginxConfig(constant.NginxScopeServer, nginxParams, &website) + } + params := getNginxParams(req.Params, keys) + if req.Operate == constant.ConfigNew { + if _, ok := dto.StaticFileKeyMap[req.Scope]; ok { + params = getNginxParamsFromStaticFile(req.Scope, params) + } + } + return updateNginxConfig(constant.NginxScopeServer, params, &website) +} + +func (w WebsiteService) GetWebsiteNginxConfig(websiteId uint, configType string) (response.FileInfo, error) { + website, err := websiteRepo.GetFirst(commonRepo.WithByID(websiteId)) + if err != nil { + return response.FileInfo{}, err + } + configPath := "" + switch configType { + case constant.AppOpenresty: + nginxApp, err := appRepo.GetFirst(appRepo.WithKey(constant.AppOpenresty)) + if err != nil { + return response.FileInfo{}, err + } + nginxInstall, err := appInstallRepo.GetFirst(appInstallRepo.WithAppId(nginxApp.ID)) + if err != nil { + return response.FileInfo{}, err + } + configPath = path.Join(nginxInstall.GetPath(), "conf", "conf.d", website.Alias+".conf") + case constant.ConfigFPM: + runtimeInstall, err := appInstallRepo.GetFirst(commonRepo.WithByID(website.AppInstallID)) + if err != nil { + return response.FileInfo{}, err + } + configPath = path.Join(runtimeInstall.GetPath(), "conf", "php-fpm.conf") + case constant.ConfigPHP: + runtimeInstall, err := appInstallRepo.GetFirst(commonRepo.WithByID(website.AppInstallID)) + if err != nil { + return response.FileInfo{}, err + } + configPath = path.Join(runtimeInstall.GetPath(), "conf", "php.ini") + } + info, err := files.NewFileInfo(files.FileOption{ + Path: configPath, + Expand: true, + }) + if err != nil { + return response.FileInfo{}, err + } + return response.FileInfo{FileInfo: *info}, nil +} + +func (w WebsiteService) GetWebsiteHTTPS(websiteId uint) (response.WebsiteHTTPS, error) { + website, err := websiteRepo.GetFirst(commonRepo.WithByID(websiteId)) + if err != nil { + return response.WebsiteHTTPS{}, err + } + var res response.WebsiteHTTPS + if website.WebsiteSSLID == 0 { + res.Enable = false + return res, nil + } + websiteSSL, err := websiteSSLRepo.GetFirst(commonRepo.WithByID(website.WebsiteSSLID)) + if err != nil { + return response.WebsiteHTTPS{}, err + } + res.SSL = *websiteSSL + res.Enable = true + if website.HttpConfig != "" { + res.HttpConfig = website.HttpConfig + } else { + res.HttpConfig = constant.HTTPToHTTPS + } + params, err := getNginxParamsByKeys(constant.NginxScopeServer, []string{"ssl_protocols", "ssl_ciphers", "add_header"}, &website) + if err != nil { + return res, err + } + for _, p := range params { + if p.Name == "ssl_protocols" { + res.SSLProtocol = p.Params + } + if p.Name == "ssl_ciphers" { + res.Algorithm = p.Params[0] + } + if p.Name == "add_header" && len(p.Params) > 0 && p.Params[0] == "Strict-Transport-Security" { + res.Hsts = true + } + } + return res, nil +} + +func (w WebsiteService) OpWebsiteHTTPS(ctx context.Context, req request.WebsiteHTTPSOp) (*response.WebsiteHTTPS, error) { + website, err := websiteRepo.GetFirst(commonRepo.WithByID(req.WebsiteID)) + if err != nil { + return nil, err + } + var ( + res response.WebsiteHTTPS + websiteSSL model.WebsiteSSL + ) + nginxInstall, err := getAppInstallByKey(constant.AppOpenresty) + if err != nil { + return nil, err + } + if err = ChangeHSTSConfig(req.Hsts, nginxInstall, website); err != nil { + return nil, err + } + res.Enable = req.Enable + res.SSLProtocol = req.SSLProtocol + res.Algorithm = req.Algorithm + if !req.Enable { + website.Protocol = constant.ProtocolHTTP + website.WebsiteSSLID = 0 + _, httpsPort, err := getAppInstallPort(constant.AppOpenresty) + if err != nil { + return nil, err + } + httpsPortStr := strconv.Itoa(httpsPort) + if err := deleteListenAndServerName(website, []string{httpsPortStr, "[::]:" + httpsPortStr}, []string{}); err != nil { + return nil, err + } + nginxParams := getNginxParamsFromStaticFile(dto.SSL, nil) + nginxParams = append(nginxParams, + dto.NginxParam{ + Name: "if", + Params: []string{"($scheme", "=", "http)"}, + }, + dto.NginxParam{ + Name: "ssl_certificate", + }, + dto.NginxParam{ + Name: "ssl_certificate_key", + }, + dto.NginxParam{ + Name: "ssl_protocols", + }, + dto.NginxParam{ + Name: "ssl_ciphers", + }, + ) + if err = deleteNginxConfig(constant.NginxScopeServer, nginxParams, &website); err != nil { + return nil, err + } + if err = websiteRepo.Save(ctx, &website); err != nil { + return nil, err + } + return nil, nil + } + + if req.Type == constant.SSLExisted { + websiteModel, err := websiteSSLRepo.GetFirst(commonRepo.WithByID(req.WebsiteSSLID)) + if err != nil { + return nil, err + } + website.WebsiteSSLID = websiteModel.ID + res.SSL = *websiteModel + websiteSSL = *websiteModel + } + if req.Type == constant.SSLManual { + var ( + certificate string + privateKey string + ) + switch req.ImportType { + case "paste": + certificate = req.Certificate + privateKey = req.PrivateKey + case "local": + fileOp := files.NewFileOp() + if !fileOp.Stat(req.PrivateKeyPath) { + return nil, buserr.New("ErrSSLKeyNotFound") + } + if !fileOp.Stat(req.CertificatePath) { + return nil, buserr.New("ErrSSLCertificateNotFound") + } + if content, err := fileOp.GetContent(req.PrivateKeyPath); err != nil { + return nil, err + } else { + privateKey = string(content) + } + if content, err := fileOp.GetContent(req.CertificatePath); err != nil { + return nil, err + } else { + certificate = string(content) + } + } + + privateKeyCertBlock, _ := pem.Decode([]byte(privateKey)) + if privateKeyCertBlock == nil { + return nil, buserr.New("ErrSSLKeyFormat") + } + + certBlock, _ := pem.Decode([]byte(certificate)) + if certBlock == nil { + return nil, buserr.New("ErrSSLCertificateFormat") + } + cert, err := x509.ParseCertificate(certBlock.Bytes) + if err != nil { + return nil, err + } + websiteSSL.ExpireDate = cert.NotAfter + websiteSSL.StartDate = cert.NotBefore + websiteSSL.Type = cert.Issuer.CommonName + if len(cert.Issuer.Organization) > 0 { + websiteSSL.Organization = cert.Issuer.Organization[0] + } else { + websiteSSL.Organization = cert.Issuer.CommonName + } + if len(cert.DNSNames) > 0 { + websiteSSL.PrimaryDomain = cert.DNSNames[0] + websiteSSL.Domains = strings.Join(cert.DNSNames, ",") + } + websiteSSL.Provider = constant.Manual + websiteSSL.PrivateKey = privateKey + websiteSSL.Pem = certificate + + res.SSL = websiteSSL + } + + website.Protocol = constant.ProtocolHTTPS + if err := applySSL(website, websiteSSL, req); err != nil { + return nil, err + } + website.HttpConfig = req.HttpConfig + + if websiteSSL.ID == 0 { + if err := websiteSSLRepo.Create(ctx, &websiteSSL); err != nil { + return nil, err + } + website.WebsiteSSLID = websiteSSL.ID + } + if err := websiteRepo.Save(ctx, &website); err != nil { + return nil, err + } + return &res, nil +} + +func (w WebsiteService) PreInstallCheck(req request.WebsiteInstallCheckReq) ([]response.WebsitePreInstallCheck, error) { + var ( + res []response.WebsitePreInstallCheck + checkIds []uint + showErr = false + ) + + app, err := appRepo.GetFirst(appRepo.WithKey(constant.AppOpenresty)) + if err != nil { + return nil, err + } + appInstall, _ := appInstallRepo.GetFirst(appInstallRepo.WithAppId(app.ID)) + if reflect.DeepEqual(appInstall, model.AppInstall{}) { + res = append(res, response.WebsitePreInstallCheck{ + Name: appInstall.Name, + AppName: app.Name, + Status: buserr.WithDetail(constant.ErrNotInstall, app.Name, nil).Error(), + Version: appInstall.Version, + }) + showErr = true + } else { + checkIds = append(req.InstallIds, appInstall.ID) + } + if len(checkIds) > 0 { + installList, _ := appInstallRepo.ListBy(commonRepo.WithIdsIn(checkIds)) + for _, install := range installList { + if err = syncAppInstallStatus(&install, false); err != nil { + return nil, err + } + res = append(res, response.WebsitePreInstallCheck{ + Name: install.Name, + Status: install.Status, + Version: install.Version, + AppName: install.App.Name, + }) + if install.Status != constant.Running { + showErr = true + } + } + } + if showErr { + return res, nil + } + return nil, nil +} + +func (w WebsiteService) UpdateNginxConfigFile(req request.WebsiteNginxUpdate) error { + website, err := websiteRepo.GetFirst(commonRepo.WithByID(req.ID)) + if err != nil { + return err + } + nginxFull, err := getNginxFull(&website) + if err != nil { + return err + } + filePath := nginxFull.SiteConfig.FilePath + if err := files.NewFileOp().WriteFile(filePath, strings.NewReader(req.Content), 0755); err != nil { + return err + } + return nginxCheckAndReload(nginxFull.SiteConfig.OldContent, filePath, nginxFull.Install.ContainerName) +} + +func (w WebsiteService) OpWebsiteLog(req request.WebsiteLogReq) (*response.WebsiteLog, error) { + website, err := websiteRepo.GetFirst(commonRepo.WithByID(req.ID)) + if err != nil { + return nil, err + } + nginx, err := getNginxFull(&website) + if err != nil { + return nil, err + } + sitePath := path.Join(nginx.SiteDir, "sites", website.Alias) + res := &response.WebsiteLog{ + Content: "", + } + switch req.Operate { + case constant.GetLog: + switch req.LogType { + case constant.AccessLog: + res.Enable = website.AccessLog + if !website.AccessLog { + return res, nil + } + case constant.ErrorLog: + res.Enable = website.ErrorLog + if !website.ErrorLog { + return res, nil + } + } + filePath := path.Join(sitePath, "log", req.LogType) + lines, end, _, err := files.ReadFileByLine(filePath, req.Page, req.PageSize, false) + if err != nil { + return nil, err + } + res.End = end + res.Path = filePath + res.Content = strings.Join(lines, "\n") + return res, nil + case constant.DisableLog: + key := "access_log" + switch req.LogType { + case constant.AccessLog: + website.AccessLog = false + case constant.ErrorLog: + key = "error_log" + website.ErrorLog = false + } + var nginxParams []dto.NginxParam + nginxParams = append(nginxParams, dto.NginxParam{ + Name: key, + Params: []string{"off"}, + }) + + if err := updateNginxConfig(constant.NginxScopeServer, nginxParams, &website); err != nil { + return nil, err + } + if err := websiteRepo.Save(context.Background(), &website); err != nil { + return nil, err + } + case constant.EnableLog: + key := "access_log" + logPath := path.Join("/www", "sites", website.Alias, "log", req.LogType) + params := []string{logPath} + switch req.LogType { + case constant.AccessLog: + params = append(params, "main") + website.AccessLog = true + case constant.ErrorLog: + key = "error_log" + website.ErrorLog = true + } + if err := updateNginxConfig(constant.NginxScopeServer, []dto.NginxParam{{Name: key, Params: params}}, &website); err != nil { + return nil, err + } + if err := websiteRepo.Save(context.Background(), &website); err != nil { + return nil, err + } + case constant.DeleteLog: + logPath := path.Join(nginx.Install.GetPath(), "www", "sites", website.Alias, "log", req.LogType) + if err := files.NewFileOp().WriteFile(logPath, strings.NewReader(""), 0755); err != nil { + return nil, err + } + } + return res, nil +} + +func (w WebsiteService) ChangeDefaultServer(id uint) error { + defaultWebsite, _ := websiteRepo.GetFirst(websiteRepo.WithDefaultServer()) + if defaultWebsite.ID > 0 { + params, err := getNginxParamsByKeys(constant.NginxScopeServer, []string{"listen"}, &defaultWebsite) + if err != nil { + return err + } + var changeParams []dto.NginxParam + for _, param := range params { + paramLen := len(param.Params) + var newParam []string + if paramLen > 1 && param.Params[paramLen-1] == components.DefaultServer { + newParam = param.Params[:paramLen-1] + } + changeParams = append(changeParams, dto.NginxParam{ + Name: param.Name, + Params: newParam, + }) + } + if err := updateNginxConfig(constant.NginxScopeServer, changeParams, &defaultWebsite); err != nil { + return err + } + defaultWebsite.DefaultServer = false + if err := websiteRepo.Save(context.Background(), &defaultWebsite); err != nil { + return err + } + } + if id > 0 { + website, err := websiteRepo.GetFirst(commonRepo.WithByID(id)) + if err != nil { + return err + } + params, err := getNginxParamsByKeys(constant.NginxScopeServer, []string{"listen"}, &website) + if err != nil { + return err + } + httpPort, httpsPort, err := getAppInstallPort(constant.AppOpenresty) + if err != nil { + return err + } + + var changeParams []dto.NginxParam + for _, param := range params { + paramLen := len(param.Params) + bind := param.Params[0] + var newParam []string + if bind == strconv.Itoa(httpPort) || bind == strconv.Itoa(httpsPort) || bind == "[::]:"+strconv.Itoa(httpPort) || bind == "[::]:"+strconv.Itoa(httpsPort) { + if param.Params[paramLen-1] == components.DefaultServer { + newParam = param.Params + } else { + newParam = append(param.Params, components.DefaultServer) + } + } + changeParams = append(changeParams, dto.NginxParam{ + Name: param.Name, + Params: newParam, + }) + } + if err := updateNginxConfig(constant.NginxScopeServer, changeParams, &website); err != nil { + return err + } + website.DefaultServer = true + return websiteRepo.Save(context.Background(), &website) + } + return nil +} + +func (w WebsiteService) GetPHPConfig(id uint) (*response.PHPConfig, error) { + website, err := websiteRepo.GetFirst(commonRepo.WithByID(id)) + if err != nil { + return nil, err + } + appInstall, err := appInstallRepo.GetFirst(commonRepo.WithByID(website.AppInstallID)) + if err != nil { + return nil, err + } + phpConfigPath := path.Join(appInstall.GetPath(), "conf", "php.ini") + fileOp := files.NewFileOp() + if !fileOp.Stat(phpConfigPath) { + return nil, buserr.WithMap("ErrFileNotFound", map[string]interface{}{"name": "php.ini"}, nil) + } + params := make(map[string]string) + configFile, err := fileOp.OpenFile(phpConfigPath) + if err != nil { + return nil, err + } + defer configFile.Close() + scanner := bufio.NewScanner(configFile) + for scanner.Scan() { + line := strings.TrimSpace(scanner.Text()) + if strings.HasPrefix(line, ";") { + continue + } + matches := regexp.MustCompile(`^\s*([a-z_]+)\s*=\s*(.*)$`).FindStringSubmatch(line) + if len(matches) == 3 { + params[matches[1]] = matches[2] + } + } + cfg, err := ini.Load(phpConfigPath) + if err != nil { + return nil, err + } + phpConfig, err := cfg.GetSection("PHP") + if err != nil { + return nil, err + } + disableFunctionStr := phpConfig.Key("disable_functions").Value() + res := &response.PHPConfig{Params: params} + if disableFunctionStr != "" { + disableFunctions := strings.Split(disableFunctionStr, ",") + if len(disableFunctions) > 0 { + res.DisableFunctions = disableFunctions + } + } + uploadMaxSize := phpConfig.Key("upload_max_filesize").Value() + if uploadMaxSize != "" { + res.UploadMaxSize = uploadMaxSize + } + return res, nil +} + +func (w WebsiteService) UpdatePHPConfig(req request.WebsitePHPConfigUpdate) (err error) { + website, err := websiteRepo.GetFirst(commonRepo.WithByID(req.ID)) + if err != nil { + return err + } + appInstall, err := appInstallRepo.GetFirst(commonRepo.WithByID(website.AppInstallID)) + if err != nil { + return err + } + phpConfigPath := path.Join(appInstall.GetPath(), "conf", "php.ini") + fileOp := files.NewFileOp() + if !fileOp.Stat(phpConfigPath) { + return buserr.WithMap("ErrFileNotFound", map[string]interface{}{"name": "php.ini"}, nil) + } + configFile, err := fileOp.OpenFile(phpConfigPath) + if err != nil { + return err + } + defer configFile.Close() + + contentBytes, err := fileOp.GetContent(phpConfigPath) + if err != nil { + return err + } + + content := string(contentBytes) + lines := strings.Split(content, "\n") + for i, line := range lines { + if strings.HasPrefix(line, ";") { + continue + } + switch req.Scope { + case "params": + for key, value := range req.Params { + pattern := "^" + regexp.QuoteMeta(key) + "\\s*=\\s*.*$" + if matched, _ := regexp.MatchString(pattern, line); matched { + lines[i] = key + " = " + value + } + } + case "disable_functions": + pattern := "^" + regexp.QuoteMeta("disable_functions") + "\\s*=\\s*.*$" + if matched, _ := regexp.MatchString(pattern, line); matched { + lines[i] = "disable_functions" + " = " + strings.Join(req.DisableFunctions, ",") + break + } + case "upload_max_filesize": + pattern := "^" + regexp.QuoteMeta("post_max_size") + "\\s*=\\s*.*$" + if matched, _ := regexp.MatchString(pattern, line); matched { + lines[i] = "post_max_size" + " = " + req.UploadMaxSize + } + patternUpload := "^" + regexp.QuoteMeta("upload_max_filesize") + "\\s*=\\s*.*$" + if matched, _ := regexp.MatchString(patternUpload, line); matched { + lines[i] = "upload_max_filesize" + " = " + req.UploadMaxSize + } + } + } + updatedContent := strings.Join(lines, "\n") + if err := fileOp.WriteFile(phpConfigPath, strings.NewReader(updatedContent), 0755); err != nil { + return err + } + + appInstallReq := request.AppInstalledOperate{ + InstallId: appInstall.ID, + Operate: constant.Restart, + } + if err = NewIAppInstalledService().Operate(appInstallReq); err != nil { + _ = fileOp.WriteFile(phpConfigPath, strings.NewReader(string(contentBytes)), 0755) + return err + } + + return nil +} + +func (w WebsiteService) UpdatePHPConfigFile(req request.WebsitePHPFileUpdate) error { + website, err := websiteRepo.GetFirst(commonRepo.WithByID(req.ID)) + if err != nil { + return err + } + if website.Type != constant.Runtime { + return nil + } + runtime, err := runtimeRepo.GetFirst(commonRepo.WithByID(website.RuntimeID)) + if err != nil { + return err + } + if runtime.Resource != constant.ResourceAppstore { + return nil + } + runtimeInstall, err := appInstallRepo.GetFirst(commonRepo.WithByID(website.AppInstallID)) + if err != nil { + return err + } + configPath := "" + if req.Type == constant.ConfigFPM { + configPath = path.Join(runtimeInstall.GetPath(), "conf", "php-fpm.conf") + } else { + configPath = path.Join(runtimeInstall.GetPath(), "conf", "php.ini") + } + if err := files.NewFileOp().WriteFile(configPath, strings.NewReader(req.Content), 0755); err != nil { + return err + } + if _, err := compose.Restart(runtimeInstall.GetComposePath()); err != nil { + return err + } + return nil +} + +func (w WebsiteService) ChangePHPVersion(req request.WebsitePHPVersionReq) error { + website, err := websiteRepo.GetFirst(commonRepo.WithByID(req.WebsiteID)) + if err != nil { + return err + } + runtime, err := runtimeRepo.GetFirst(commonRepo.WithByID(req.RuntimeID)) + if err != nil { + return err + } + oldRuntime, err := runtimeRepo.GetFirst(commonRepo.WithByID(website.RuntimeID)) + if err != nil { + return err + } + if runtime.Resource == constant.ResourceLocal || oldRuntime.Resource == constant.ResourceLocal { + return buserr.New("ErrPHPResource") + } + appInstall, err := appInstallRepo.GetFirst(commonRepo.WithByID(website.AppInstallID)) + if err != nil { + return err + } + appDetail, err := appDetailRepo.GetFirst(commonRepo.WithByID(runtime.AppDetailID)) + if err != nil { + return err + } + + envs := make(map[string]interface{}) + if err = json.Unmarshal([]byte(appInstall.Env), &envs); err != nil { + return err + } + if out, err := compose.Down(appInstall.GetComposePath()); err != nil { + if out != "" { + return errors.New(out) + } + return err + } + + var ( + busErr error + fileOp = files.NewFileOp() + envPath = appInstall.GetEnvPath() + composePath = appInstall.GetComposePath() + confDir = path.Join(appInstall.GetPath(), "conf") + backupConfDir = path.Join(appInstall.GetPath(), "conf_bak") + fpmConfDir = path.Join(confDir, "php-fpm.conf") + phpDir = path.Join(constant.RuntimeDir, runtime.Type, runtime.Name, "php") + oldFmContent, _ = fileOp.GetContent(fpmConfDir) + newComposeByte []byte + ) + envParams := make(map[string]string, len(envs)) + handleMap(envs, envParams) + envParams["IMAGE_NAME"] = runtime.Image + defer func() { + if busErr != nil { + envParams["IMAGE_NAME"] = oldRuntime.Image + _ = env.Write(envParams, envPath) + _ = fileOp.WriteFile(composePath, strings.NewReader(appInstall.DockerCompose), 0775) + if fileOp.Stat(backupConfDir) { + _ = fileOp.DeleteDir(confDir) + _ = fileOp.Rename(backupConfDir, confDir) + } + } + }() + + if busErr = env.Write(envParams, envPath); busErr != nil { + return busErr + } + + newComposeByte, busErr = changeServiceName(appDetail.DockerCompose, appInstall.ServiceName) + if busErr != nil { + return err + } + + if busErr = fileOp.WriteFile(composePath, bytes.NewReader(newComposeByte), 0775); busErr != nil { + return busErr + } + if !req.RetainConfig { + if busErr = fileOp.Rename(confDir, backupConfDir); busErr != nil { + return busErr + } + _ = fileOp.CreateDir(confDir, 0755) + if busErr = fileOp.CopyFile(path.Join(phpDir, "php-fpm.conf"), confDir); busErr != nil { + return busErr + } + if busErr = fileOp.CopyFile(path.Join(phpDir, "php.ini"), confDir); busErr != nil { + _ = fileOp.WriteFile(fpmConfDir, bytes.NewReader(oldFmContent), 0775) + return busErr + } + } + + if out, err := compose.Up(appInstall.GetComposePath()); err != nil { + if out != "" { + busErr = errors.New(out) + return busErr + } + busErr = err + return busErr + } + + _ = fileOp.DeleteDir(backupConfDir) + + appInstall.AppDetailId = runtime.AppDetailID + appInstall.AppId = appDetail.AppId + appInstall.Version = appDetail.Version + appInstall.DockerCompose = string(newComposeByte) + + _ = appInstallRepo.Save(context.Background(), &appInstall) + website.RuntimeID = req.RuntimeID + return websiteRepo.Save(context.Background(), &website) +} + +func (w WebsiteService) UpdateRewriteConfig(req request.NginxRewriteUpdate) error { + website, err := websiteRepo.GetFirst(commonRepo.WithByID(req.WebsiteID)) + if err != nil { + return err + } + nginxFull, err := getNginxFull(&website) + if err != nil { + return err + } + includePath := fmt.Sprintf("/www/sites/%s/rewrite/%s.conf", website.Alias, website.PrimaryDomain) + absolutePath := path.Join(nginxFull.Install.GetPath(), includePath) + fileOp := files.NewFileOp() + var oldRewriteContent []byte + if !fileOp.Stat(path.Dir(absolutePath)) { + if err := fileOp.CreateDir(path.Dir(absolutePath), 0755); err != nil { + return err + } + } + if !fileOp.Stat(absolutePath) { + if err := fileOp.CreateFile(absolutePath); err != nil { + return err + } + } else { + oldRewriteContent, err = fileOp.GetContent(absolutePath) + if err != nil { + return err + } + } + if err := fileOp.WriteFile(absolutePath, strings.NewReader(req.Content), 0755); err != nil { + return err + } + + if err := updateNginxConfig(constant.NginxScopeServer, []dto.NginxParam{{Name: "include", Params: []string{includePath}}}, &website); err != nil { + _ = fileOp.WriteFile(absolutePath, bytes.NewReader(oldRewriteContent), 0755) + return err + } + website.Rewrite = req.Name + return websiteRepo.Save(context.Background(), &website) +} + +func (w WebsiteService) GetRewriteConfig(req request.NginxRewriteReq) (*response.NginxRewriteRes, error) { + website, err := websiteRepo.GetFirst(commonRepo.WithByID(req.WebsiteID)) + if err != nil { + return nil, err + } + var contentByte []byte + if req.Name == "current" { + nginxInstall, err := getAppInstallByKey(constant.AppOpenresty) + if err != nil { + return nil, err + } + rewriteConfPath := path.Join(nginxInstall.GetPath(), "www", "sites", website.Alias, "rewrite", fmt.Sprintf("%s.conf", website.PrimaryDomain)) + fileOp := files.NewFileOp() + if fileOp.Stat(rewriteConfPath) { + contentByte, err = fileOp.GetContent(rewriteConfPath) + if err != nil { + return nil, err + } + } + } else { + rewriteFile := fmt.Sprintf("rewrite/%s.conf", strings.ToLower(req.Name)) + contentByte, err = nginx_conf.Rewrites.ReadFile(rewriteFile) + if err != nil { + return nil, err + } + } + return &response.NginxRewriteRes{ + Content: string(contentByte), + }, err +} + +func (w WebsiteService) UpdateSiteDir(req request.WebsiteUpdateDir) error { + website, err := websiteRepo.GetFirst(commonRepo.WithByID(req.ID)) + if err != nil { + return err + } + runDir := req.SiteDir + siteDir := path.Join("/www/sites", website.Alias, "index") + if req.SiteDir != "/" { + siteDir = fmt.Sprintf("%s%s", siteDir, req.SiteDir) + } + if err := updateNginxConfig(constant.NginxScopeServer, []dto.NginxParam{{Name: "root", Params: []string{siteDir}}}, &website); err != nil { + return err + } + website.SiteDir = runDir + return websiteRepo.Save(context.Background(), &website) +} + +func (w WebsiteService) UpdateSitePermission(req request.WebsiteUpdateDirPermission) error { + website, err := websiteRepo.GetFirst(commonRepo.WithByID(req.ID)) + if err != nil { + return err + } + nginxInstall, err := getAppInstallByKey(constant.AppOpenresty) + if err != nil { + return err + } + absoluteIndexPath := path.Join(nginxInstall.GetPath(), "www", "sites", website.Alias, "index") + chownCmd := fmt.Sprintf("chown -R %s:%s %s", req.User, req.Group, absoluteIndexPath) + if cmd.HasNoPasswordSudo() { + chownCmd = fmt.Sprintf("sudo %s", chownCmd) + } + if out, err := cmd.ExecWithTimeOut(chownCmd, 10*time.Second); err != nil { + if out != "" { + return errors.New(out) + } + return err + } + website.User = req.User + website.Group = req.Group + return websiteRepo.Save(context.Background(), &website) +} + +func (w WebsiteService) OperateProxy(req request.WebsiteProxyConfig) (err error) { + var ( + website model.Website + params []response.NginxParam + nginxInstall model.AppInstall + par *parser.Parser + oldContent []byte + ) + + website, err = websiteRepo.GetFirst(commonRepo.WithByID(req.ID)) + if err != nil { + return + } + params, err = getNginxParamsByKeys(constant.NginxScopeHttp, []string{"proxy_cache"}, &website) + if err != nil { + return + } + nginxInstall, err = getAppInstallByKey(constant.AppOpenresty) + if err != nil { + return + } + fileOp := files.NewFileOp() + if len(params) == 0 || len(params[0].Params) == 0 { + commonDir := path.Join(nginxInstall.GetPath(), "www", "common", "proxy") + proxyTempPath := path.Join(commonDir, "proxy_temp_dir") + if !fileOp.Stat(proxyTempPath) { + _ = fileOp.CreateDir(proxyTempPath, 0755) + } + proxyCacheDir := path.Join(commonDir, "proxy_temp_dir") + if !fileOp.Stat(proxyCacheDir) { + _ = fileOp.CreateDir(proxyCacheDir, 0755) + } + nginxParams := getNginxParamsFromStaticFile(dto.CACHE, nil) + if err = updateNginxConfig(constant.NginxScopeHttp, nginxParams, &website); err != nil { + return + } + } + includeDir := path.Join(nginxInstall.GetPath(), "www", "sites", website.Alias, "proxy") + if !fileOp.Stat(includeDir) { + _ = fileOp.CreateDir(includeDir, 0755) + } + fileName := fmt.Sprintf("%s.conf", req.Name) + includePath := path.Join(includeDir, fileName) + backName := fmt.Sprintf("%s.bak", req.Name) + backPath := path.Join(includeDir, backName) + + if req.Operate == "create" && (fileOp.Stat(includePath) || fileOp.Stat(backPath)) { + err = buserr.New(constant.ErrNameIsExist) + return + } + + defer func() { + if err != nil { + switch req.Operate { + case "create": + _ = fileOp.DeleteFile(includePath) + case "edit": + _ = fileOp.WriteFile(includePath, bytes.NewReader(oldContent), 0755) + } + } + }() + + var config *components.Config + + switch req.Operate { + case "create": + config, err = parser.NewStringParser(string(nginx_conf.Proxy)).Parse() + if err != nil { + return + } + case "edit": + par, err = parser.NewParser(includePath) + if err != nil { + return + } + config, err = par.Parse() + if err != nil { + return + } + oldContent, err = fileOp.GetContent(includePath) + if err != nil { + return + } + case "delete": + _ = fileOp.DeleteFile(includePath) + _ = fileOp.DeleteFile(backPath) + return updateNginxConfig(constant.NginxScopeServer, nil, &website) + case "disable": + _ = fileOp.Rename(includePath, backPath) + return updateNginxConfig(constant.NginxScopeServer, nil, &website) + case "enable": + _ = fileOp.Rename(backPath, includePath) + return updateNginxConfig(constant.NginxScopeServer, nil, &website) + } + + config.FilePath = includePath + directives := config.Directives + location, ok := directives[0].(*components.Location) + if !ok { + err = errors.New("error") + return + } + location.UpdateDirective("proxy_pass", []string{req.ProxyPass}) + location.UpdateDirective("proxy_set_header", []string{"Host", req.ProxyHost}) + location.ChangePath(req.Modifier, req.Match) + if req.Cache { + location.AddCache(req.CacheTime, req.CacheUnit) + } else { + location.RemoveCache() + } + if len(req.Replaces) > 0 { + location.AddSubFilter(req.Replaces) + } else { + location.RemoveSubFilter() + } + if req.SNI { + location.UpdateDirective("proxy_ssl_server_name", []string{"on"}) + } else { + location.UpdateDirective("proxy_ssl_server_name", []string{"off"}) + } + if err = nginx.WriteConfig(config, nginx.IndentedStyle); err != nil { + return buserr.WithErr(constant.ErrUpdateBuWebsite, err) + } + nginxInclude := fmt.Sprintf("/www/sites/%s/proxy/*.conf", website.Alias) + if err = updateNginxConfig(constant.NginxScopeServer, []dto.NginxParam{{Name: "include", Params: []string{nginxInclude}}}, &website); err != nil { + return + } + return +} + +func (w WebsiteService) GetProxies(id uint) (res []request.WebsiteProxyConfig, err error) { + var ( + website model.Website + nginxInstall model.AppInstall + fileList response.FileInfo + ) + website, err = websiteRepo.GetFirst(commonRepo.WithByID(id)) + if err != nil { + return + } + nginxInstall, err = getAppInstallByKey(constant.AppOpenresty) + if err != nil { + return + } + includeDir := path.Join(nginxInstall.GetPath(), "www", "sites", website.Alias, "proxy") + fileOp := files.NewFileOp() + if !fileOp.Stat(includeDir) { + return + } + fileList, err = NewIFileService().GetFileList(request.FileOption{FileOption: files.FileOption{Path: includeDir, Expand: true, Page: 1, PageSize: 100}}) + if len(fileList.Items) == 0 { + return + } + var ( + content []byte + config *components.Config + ) + for _, configFile := range fileList.Items { + proxyConfig := request.WebsiteProxyConfig{ + ID: website.ID, + } + parts := strings.Split(configFile.Name, ".") + proxyConfig.Name = parts[0] + if parts[1] == "conf" { + proxyConfig.Enable = true + } else { + proxyConfig.Enable = false + } + proxyConfig.FilePath = configFile.Path + content, err = fileOp.GetContent(configFile.Path) + if err != nil { + return + } + proxyConfig.Content = string(content) + config, err = parser.NewStringParser(string(content)).Parse() + if err != nil { + return nil, err + } + directives := config.GetDirectives() + + location, ok := directives[0].(*components.Location) + if !ok { + err = errors.New("error") + return + } + proxyConfig.ProxyPass = location.ProxyPass + proxyConfig.Cache = location.Cache + if location.CacheTime > 0 { + proxyConfig.CacheTime = location.CacheTime + proxyConfig.CacheUnit = location.CacheUint + } + proxyConfig.Match = location.Match + proxyConfig.Modifier = location.Modifier + proxyConfig.ProxyHost = location.Host + proxyConfig.Replaces = location.Replaces + for _, directive := range location.Directives { + if directive.GetName() == "proxy_ssl_server_name" { + proxyConfig.SNI = directive.GetParameters()[0] == "on" + } + } + res = append(res, proxyConfig) + } + return +} + +func (w WebsiteService) UpdateProxyFile(req request.NginxProxyUpdate) (err error) { + var ( + website model.Website + nginxFull dto.NginxFull + oldRewriteContent []byte + ) + website, err = websiteRepo.GetFirst(commonRepo.WithByID(req.WebsiteID)) + if err != nil { + return err + } + nginxFull, err = getNginxFull(&website) + if err != nil { + return err + } + includePath := fmt.Sprintf("/www/sites/%s/proxy/%s.conf", website.Alias, req.Name) + absolutePath := path.Join(nginxFull.Install.GetPath(), includePath) + fileOp := files.NewFileOp() + oldRewriteContent, err = fileOp.GetContent(absolutePath) + if err != nil { + return err + } + if err = fileOp.WriteFile(absolutePath, strings.NewReader(req.Content), 0755); err != nil { + return err + } + defer func() { + if err != nil { + _ = fileOp.WriteFile(absolutePath, bytes.NewReader(oldRewriteContent), 0755) + } + }() + return updateNginxConfig(constant.NginxScopeServer, nil, &website) +} + +func (w WebsiteService) UpdateAuthBasic(req request.NginxAuthUpdate) (err error) { + var ( + website model.Website + nginxInstall model.AppInstall + params []dto.NginxParam + authContent []byte + authArray []string + ) + website, err = websiteRepo.GetFirst(commonRepo.WithByID(req.WebsiteID)) + if err != nil { + return err + } + nginxInstall, err = getAppInstallByKey(constant.AppOpenresty) + if err != nil { + return + } + authPath := fmt.Sprintf("/www/sites/%s/auth_basic/auth.pass", website.Alias) + absoluteAuthPath := path.Join(nginxInstall.GetPath(), authPath) + fileOp := files.NewFileOp() + if !fileOp.Stat(path.Dir(absoluteAuthPath)) { + _ = fileOp.CreateDir(path.Dir(absoluteAuthPath), 0755) + } + if !fileOp.Stat(absoluteAuthPath) { + _ = fileOp.CreateFile(absoluteAuthPath) + } + + params = append(params, dto.NginxParam{Name: "auth_basic", Params: []string{`"Authentication"`}}) + params = append(params, dto.NginxParam{Name: "auth_basic_user_file", Params: []string{authPath}}) + authContent, err = fileOp.GetContent(absoluteAuthPath) + if err != nil { + return + } + if len(authContent) > 0 { + authArray = strings.Split(string(authContent), "\n") + } + switch req.Operate { + case "disable": + return deleteNginxConfig(constant.NginxScopeServer, params, &website) + case "enable": + return updateNginxConfig(constant.NginxScopeServer, params, &website) + case "create": + for _, line := range authArray { + authParams := strings.Split(line, ":") + username := authParams[0] + if username == req.Username { + err = buserr.New(constant.ErrUsernameIsExist) + return + } + } + var passwdHash []byte + passwdHash, err = bcrypt.GenerateFromPassword([]byte(req.Password), bcrypt.DefaultCost) + if err != nil { + return + } + line := fmt.Sprintf("%s:%s\n", req.Username, passwdHash) + if req.Remark != "" { + line = fmt.Sprintf("%s:%s:%s\n", req.Username, passwdHash, req.Remark) + } + authArray = append(authArray, line) + case "edit": + userExist := false + for index, line := range authArray { + authParams := strings.Split(line, ":") + username := authParams[0] + if username == req.Username { + userExist = true + var passwdHash []byte + passwdHash, err = bcrypt.GenerateFromPassword([]byte(req.Password), bcrypt.DefaultCost) + if err != nil { + return + } + userPasswd := fmt.Sprintf("%s:%s\n", req.Username, passwdHash) + if req.Remark != "" { + userPasswd = fmt.Sprintf("%s:%s:%s\n", req.Username, passwdHash, req.Remark) + } + authArray[index] = userPasswd + } + } + if !userExist { + err = buserr.New(constant.ErrUsernameIsNotExist) + return + } + case "delete": + deleteIndex := -1 + for index, line := range authArray { + authParams := strings.Split(line, ":") + username := authParams[0] + if username == req.Username { + deleteIndex = index + } + } + if deleteIndex < 0 { + return + } + authArray = append(authArray[:deleteIndex], authArray[deleteIndex+1:]...) + } + + var passFile *os.File + passFile, err = os.Create(absoluteAuthPath) + if err != nil { + return + } + defer passFile.Close() + writer := bufio.NewWriter(passFile) + for _, line := range authArray { + if line == "" { + continue + } + _, err = writer.WriteString(line + "\n") + if err != nil { + return + } + } + err = writer.Flush() + if err != nil { + return + } + authContent, err = fileOp.GetContent(absoluteAuthPath) + if err != nil { + return + } + if len(authContent) == 0 { + if err = deleteNginxConfig(constant.NginxScopeServer, params, &website); err != nil { + return + } + } + return +} + +func (w WebsiteService) GetAuthBasics(req request.NginxAuthReq) (res response.NginxAuthRes, err error) { + var ( + website model.Website + nginxInstall model.AppInstall + authContent []byte + nginxParams []response.NginxParam + ) + website, err = websiteRepo.GetFirst(commonRepo.WithByID(req.WebsiteID)) + if err != nil { + return + } + nginxInstall, err = getAppInstallByKey(constant.AppOpenresty) + if err != nil { + return + } + authPath := fmt.Sprintf("/www/sites/%s/auth_basic/auth.pass", website.Alias) + absoluteAuthPath := path.Join(nginxInstall.GetPath(), authPath) + fileOp := files.NewFileOp() + if !fileOp.Stat(absoluteAuthPath) { + return + } + nginxParams, err = getNginxParamsByKeys(constant.NginxScopeServer, []string{"auth_basic"}, &website) + if err != nil { + return + } + res.Enable = len(nginxParams[0].Params) > 0 + authContent, err = fileOp.GetContent(absoluteAuthPath) + authArray := strings.Split(string(authContent), "\n") + for _, line := range authArray { + if line == "" { + continue + } + params := strings.Split(line, ":") + auth := dto.NginxAuth{ + Username: params[0], + } + if len(params) == 3 { + auth.Remark = params[2] + } + res.Items = append(res.Items, auth) + } + return +} + +func (w WebsiteService) UpdateAntiLeech(req request.NginxAntiLeechUpdate) (err error) { + website, err := websiteRepo.GetFirst(commonRepo.WithByID(req.WebsiteID)) + if err != nil { + return + } + nginxFull, err := getNginxFull(&website) + if err != nil { + return + } + fileOp := files.NewFileOp() + backupContent, err := fileOp.GetContent(nginxFull.SiteConfig.Config.FilePath) + if err != nil { + return + } + block := nginxFull.SiteConfig.Config.FindServers()[0] + locations := block.FindDirectives("location") + for _, location := range locations { + loParams := location.GetParameters() + if len(loParams) > 1 || loParams[0] == "~" { + extendStr := loParams[1] + if strings.HasPrefix(extendStr, `.*\.(`) && strings.HasSuffix(extendStr, `)$`) { + block.RemoveDirective("location", loParams) + } + } + } + if req.Enable { + exts := strings.Split(req.Extends, ",") + newDirective := components.Directive{ + Name: "location", + Parameters: []string{"~", fmt.Sprintf(`.*\.(%s)$`, strings.Join(exts, "|"))}, + } + + newBlock := &components.Block{} + newBlock.Directives = make([]components.IDirective, 0) + if req.Cache { + newBlock.Directives = append(newBlock.Directives, &components.Directive{ + Name: "expires", + Parameters: []string{strconv.Itoa(req.CacheTime) + req.CacheUint}, + }) + } + newBlock.Directives = append(newBlock.Directives, &components.Directive{ + Name: "log_not_found", + Parameters: []string{"off"}, + }) + validDir := &components.Directive{ + Name: "valid_referers", + Parameters: []string{}, + } + if req.NoneRef { + validDir.Parameters = append(validDir.Parameters, "none") + } + if len(req.ServerNames) > 0 { + validDir.Parameters = append(validDir.Parameters, strings.Join(req.ServerNames, " ")) + } + newBlock.Directives = append(newBlock.Directives, validDir) + + ifDir := &components.Directive{ + Name: "if", + Parameters: []string{"($invalid_referer)"}, + } + ifDir.Block = &components.Block{ + Directives: []components.IDirective{ + &components.Directive{ + Name: "return", + Parameters: []string{req.Return}, + }, + &components.Directive{ + Name: "access_log", + Parameters: []string{"off"}, + }, + }, + } + newBlock.Directives = append(newBlock.Directives, ifDir) + newDirective.Block = newBlock + block.Directives = append(block.Directives, &newDirective) + } + + if err = nginx.WriteConfig(nginxFull.SiteConfig.Config, nginx.IndentedStyle); err != nil { + return + } + if err = updateNginxConfig(constant.NginxScopeServer, nil, &website); err != nil { + _ = fileOp.WriteFile(nginxFull.SiteConfig.Config.FilePath, bytes.NewReader(backupContent), 0755) + return + } + return +} + +func (w WebsiteService) GetAntiLeech(id uint) (*response.NginxAntiLeechRes, error) { + website, err := websiteRepo.GetFirst(commonRepo.WithByID(id)) + if err != nil { + return nil, err + } + nginxFull, err := getNginxFull(&website) + if err != nil { + return nil, err + } + res := &response.NginxAntiLeechRes{ + LogEnable: true, + ServerNames: []string{}, + } + block := nginxFull.SiteConfig.Config.FindServers()[0] + locations := block.FindDirectives("location") + for _, location := range locations { + loParams := location.GetParameters() + if len(loParams) > 1 || loParams[0] == "~" { + extendStr := loParams[1] + if strings.HasPrefix(extendStr, `.*\.(`) && strings.HasSuffix(extendStr, `)$`) { + str1 := strings.TrimPrefix(extendStr, `.*\.(`) + str2 := strings.TrimSuffix(str1, ")$") + res.Extends = strings.Join(strings.Split(str2, "|"), ",") + } + } + lDirectives := location.GetBlock().GetDirectives() + for _, lDir := range lDirectives { + if lDir.GetName() == "valid_referers" { + res.Enable = true + params := lDir.GetParameters() + for _, param := range params { + if param == "none" { + res.NoneRef = true + continue + } + if param == "blocked" { + res.Blocked = true + continue + } + if param == "server_names" { + continue + } + res.ServerNames = append(res.ServerNames, param) + } + } + if lDir.GetName() == "if" && lDir.GetParameters()[0] == "($invalid_referer)" { + directives := lDir.GetBlock().GetDirectives() + for _, dir := range directives { + if dir.GetName() == "return" { + res.Return = strings.Join(dir.GetParameters(), " ") + } + if dir.GetName() == "access_log" { + if strings.Join(dir.GetParameters(), "") == "off" { + res.LogEnable = false + } + } + } + } + if lDir.GetName() == "expires" { + res.Cache = true + re := regexp.MustCompile(`^(\d+)(\w+)$`) + matches := re.FindStringSubmatch(lDir.GetParameters()[0]) + if matches == nil { + continue + } + cacheTime, err := strconv.Atoi(matches[1]) + if err != nil { + continue + } + unit := matches[2] + res.CacheUint = unit + res.CacheTime = cacheTime + } + } + } + return res, nil +} + +func (w WebsiteService) OperateRedirect(req request.NginxRedirectReq) (err error) { + var ( + website model.Website + nginxInstall model.AppInstall + oldContent []byte + ) + + website, err = websiteRepo.GetFirst(commonRepo.WithByID(req.WebsiteID)) + if err != nil { + return err + } + nginxInstall, err = getAppInstallByKey(constant.AppOpenresty) + if err != nil { + return + } + includeDir := path.Join(nginxInstall.GetPath(), "www", "sites", website.Alias, "redirect") + fileOp := files.NewFileOp() + if !fileOp.Stat(includeDir) { + _ = fileOp.CreateDir(includeDir, 0755) + } + fileName := fmt.Sprintf("%s.conf", req.Name) + includePath := path.Join(includeDir, fileName) + backName := fmt.Sprintf("%s.bak", req.Name) + backPath := path.Join(includeDir, backName) + + if req.Operate == "create" && (fileOp.Stat(includePath) || fileOp.Stat(backPath)) { + err = buserr.New(constant.ErrNameIsExist) + return + } + + defer func() { + if err != nil { + switch req.Operate { + case "create": + _ = fileOp.DeleteFile(includePath) + case "edit": + _ = fileOp.WriteFile(includePath, bytes.NewReader(oldContent), 0755) + } + } + }() + + var ( + config *components.Config + oldPar *parser.Parser + ) + + switch req.Operate { + case "create": + config = &components.Config{} + case "edit": + oldPar, err = parser.NewParser(includePath) + if err != nil { + return + } + config, err = oldPar.Parse() + if err != nil { + return + } + oldContent, err = fileOp.GetContent(includePath) + if err != nil { + return + } + case "delete": + _ = fileOp.DeleteFile(includePath) + _ = fileOp.DeleteFile(backPath) + return updateNginxConfig(constant.NginxScopeServer, nil, &website) + case "disable": + _ = fileOp.Rename(includePath, backPath) + return updateNginxConfig(constant.NginxScopeServer, nil, &website) + case "enable": + _ = fileOp.Rename(backPath, includePath) + return updateNginxConfig(constant.NginxScopeServer, nil, &website) + } + + target := req.Target + block := &components.Block{} + + switch req.Type { + case "path": + if req.KeepPath { + target = req.Target + "$1" + } else { + target = req.Target + "?" + } + redirectKey := "permanent" + if req.Redirect == "302" { + redirectKey = "redirect" + } + block = &components.Block{ + Directives: []components.IDirective{ + &components.Directive{ + Name: "rewrite", + Parameters: []string{fmt.Sprintf("^%s(.*)", req.Path), target, redirectKey}, + }, + }, + } + case "domain": + if req.KeepPath { + target = req.Target + "$request_uri" + } + returnBlock := &components.Block{ + Directives: []components.IDirective{ + &components.Directive{ + Name: "return", + Parameters: []string{req.Redirect, target}, + }, + }, + } + for _, domain := range req.Domains { + block.Directives = append(block.Directives, &components.Directive{ + Name: "if", + Parameters: []string{"($host", "~", fmt.Sprintf("'^%s')", domain)}, + Block: returnBlock, + }) + } + case "404": + if req.RedirectRoot { + target = "/" + } + block = &components.Block{ + Directives: []components.IDirective{ + &components.Directive{ + Name: "error_page", + Parameters: []string{"404", "=", "@notfound"}, + }, + &components.Directive{ + Name: "location", + Parameters: []string{"@notfound"}, + Block: &components.Block{ + Directives: []components.IDirective{ + &components.Directive{ + Name: "return", + Parameters: []string{req.Redirect, target}, + }, + }, + }, + }, + }, + } + } + config.FilePath = includePath + config.Block = block + + if err = nginx.WriteConfig(config, nginx.IndentedStyle); err != nil { + return buserr.WithErr(constant.ErrUpdateBuWebsite, err) + } + + nginxInclude := fmt.Sprintf("/www/sites/%s/redirect/*.conf", website.Alias) + if err = updateNginxConfig(constant.NginxScopeServer, []dto.NginxParam{{Name: "include", Params: []string{nginxInclude}}}, &website); err != nil { + return + } + return +} + +func (w WebsiteService) GetRedirect(id uint) (res []response.NginxRedirectConfig, err error) { + var ( + website model.Website + nginxInstall model.AppInstall + fileList response.FileInfo + ) + website, err = websiteRepo.GetFirst(commonRepo.WithByID(id)) + if err != nil { + return + } + nginxInstall, err = getAppInstallByKey(constant.AppOpenresty) + if err != nil { + return + } + includeDir := path.Join(nginxInstall.GetPath(), "www", "sites", website.Alias, "redirect") + fileOp := files.NewFileOp() + if !fileOp.Stat(includeDir) { + return + } + fileList, err = NewIFileService().GetFileList(request.FileOption{FileOption: files.FileOption{Path: includeDir, Expand: true, Page: 1, PageSize: 100}}) + if len(fileList.Items) == 0 { + return + } + var ( + content []byte + config *components.Config + ) + for _, configFile := range fileList.Items { + redirectConfig := response.NginxRedirectConfig{ + WebsiteID: website.ID, + } + parts := strings.Split(configFile.Name, ".") + redirectConfig.Name = parts[0] + if parts[1] == "conf" { + redirectConfig.Enable = true + } else { + redirectConfig.Enable = false + } + redirectConfig.FilePath = configFile.Path + content, err = fileOp.GetContent(configFile.Path) + if err != nil { + return + } + redirectConfig.Content = string(content) + config, err = parser.NewStringParser(string(content)).Parse() + if err != nil { + return + } + + dirs := config.GetDirectives() + if len(dirs) > 0 { + firstName := dirs[0].GetName() + switch firstName { + case "if": + for _, ifDir := range dirs { + params := ifDir.GetParameters() + if len(params) > 2 && params[0] == "($host" { + domain := strings.Trim(strings.Trim(params[2], "'"), "^") + redirectConfig.Domains = append(redirectConfig.Domains, domain) + if len(redirectConfig.Domains) > 1 { + continue + } + redirectConfig.Type = "domain" + } + childDirs := ifDir.GetBlock().GetDirectives() + for _, dir := range childDirs { + if dir.GetName() == "return" { + dirParams := dir.GetParameters() + if len(dirParams) > 1 { + redirectConfig.Redirect = dirParams[0] + if strings.HasSuffix(dirParams[1], "$request_uri") { + redirectConfig.KeepPath = true + redirectConfig.Target = strings.TrimSuffix(dirParams[1], "$request_uri") + } else { + redirectConfig.KeepPath = false + redirectConfig.Target = dirParams[1] + } + } + } + } + } + case "rewrite": + redirectConfig.Type = "path" + for _, pathDir := range dirs { + if pathDir.GetName() == "rewrite" { + params := pathDir.GetParameters() + if len(params) > 2 { + redirectConfig.Path = strings.Trim(strings.Trim(params[0], "^"), "(.*)") + if strings.HasSuffix(params[1], "$1") { + redirectConfig.KeepPath = true + redirectConfig.Target = strings.TrimSuffix(params[1], "$1") + } else { + redirectConfig.KeepPath = false + redirectConfig.Target = strings.TrimSuffix(params[1], "?") + } + if params[2] == "permanent" { + redirectConfig.Redirect = "301" + } else { + redirectConfig.Redirect = "302" + } + } + } + } + case "error_page": + redirectConfig.Type = "404" + for _, errDir := range dirs { + if errDir.GetName() == "location" { + childDirs := errDir.GetBlock().GetDirectives() + for _, dir := range childDirs { + if dir.GetName() == "return" { + dirParams := dir.GetParameters() + if len(dirParams) > 1 { + redirectConfig.Redirect = dirParams[0] + if strings.HasSuffix(dirParams[1], "$request_uri") { + redirectConfig.KeepPath = true + redirectConfig.Target = strings.TrimSuffix(dirParams[1], "$request_uri") + redirectConfig.RedirectRoot = false + } else { + redirectConfig.KeepPath = false + redirectConfig.Target = dirParams[1] + redirectConfig.RedirectRoot = redirectConfig.Target == "/" + } + } + } + } + } + } + } + } + res = append(res, redirectConfig) + } + return +} + +func (w WebsiteService) UpdateRedirectFile(req request.NginxRedirectUpdate) (err error) { + var ( + website model.Website + nginxFull dto.NginxFull + oldRewriteContent []byte + ) + website, err = websiteRepo.GetFirst(commonRepo.WithByID(req.WebsiteID)) + if err != nil { + return err + } + nginxFull, err = getNginxFull(&website) + if err != nil { + return err + } + includePath := fmt.Sprintf("/www/sites/%s/redirect/%s.conf", website.Alias, req.Name) + absolutePath := path.Join(nginxFull.Install.GetPath(), includePath) + fileOp := files.NewFileOp() + oldRewriteContent, err = fileOp.GetContent(absolutePath) + if err != nil { + return err + } + if err = fileOp.WriteFile(absolutePath, strings.NewReader(req.Content), 0755); err != nil { + return err + } + defer func() { + if err != nil { + _ = fileOp.WriteFile(absolutePath, bytes.NewReader(oldRewriteContent), 0755) + } + }() + return updateNginxConfig(constant.NginxScopeServer, nil, &website) +} + +func (w WebsiteService) LoadWebsiteDirConfig(req request.WebsiteCommonReq) (*response.WebsiteDirConfig, error) { + website, err := websiteRepo.GetFirst(commonRepo.WithByID(req.ID)) + if err != nil { + return nil, err + } + res := &response.WebsiteDirConfig{} + nginxInstall, err := getAppInstallByKey(constant.AppOpenresty) + if err != nil { + return nil, err + } + absoluteIndexPath := path.Join(nginxInstall.GetPath(), "www", "sites", website.Alias, "index") + var appFs = afero.NewOsFs() + info, err := appFs.Stat(absoluteIndexPath) + if err != nil { + return nil, err + } + res.User = strconv.FormatUint(uint64(info.Sys().(*syscall.Stat_t).Uid), 10) + res.UserGroup = strconv.FormatUint(uint64(info.Sys().(*syscall.Stat_t).Gid), 10) + + indexFiles, err := os.ReadDir(absoluteIndexPath) + if err != nil { + return nil, err + } + res.Dirs = []string{"/"} + for _, file := range indexFiles { + if !file.IsDir() { + continue + } + res.Dirs = append(res.Dirs, fmt.Sprintf("/%s", file.Name())) + fileInfo, _ := file.Info() + if fileInfo.Sys().(*syscall.Stat_t).Uid != 1000 || fileInfo.Sys().(*syscall.Stat_t).Gid != 1000 { + res.Msg = i18n.GetMsgByKey("ErrPathPermission") + } + childFiles, _ := os.ReadDir(absoluteIndexPath + "/" + file.Name()) + for _, childFile := range childFiles { + if !childFile.IsDir() { + continue + } + childInfo, _ := childFile.Info() + if childInfo.Sys().(*syscall.Stat_t).Uid != 1000 || childInfo.Sys().(*syscall.Stat_t).Gid != 1000 { + res.Msg = i18n.GetMsgByKey("ErrPathPermission") + } + res.Dirs = append(res.Dirs, fmt.Sprintf("/%s/%s", file.Name(), childFile.Name())) + } + } + + return res, nil +} + +func (w WebsiteService) GetDefaultHtml(resourceType string) (*response.WebsiteHtmlRes, error) { + nginxInstall, err := getAppInstallByKey(constant.AppOpenresty) + if err != nil { + return nil, err + } + rootPath := path.Join(nginxInstall.GetPath(), "root") + fileOp := files.NewFileOp() + defaultPath := path.Join(rootPath, "default") + if !fileOp.Stat(defaultPath) { + _ = fileOp.CreateDir(defaultPath, 0755) + } + + res := &response.WebsiteHtmlRes{} + + switch resourceType { + case "404": + resourcePath := path.Join(defaultPath, "404.html") + if content, _ := getResourceContent(fileOp, resourcePath); content != "" { + res.Content = content + return res, nil + } + res.Content = string(nginx_conf.NotFoundHTML) + return res, nil + case "php": + resourcePath := path.Join(defaultPath, "index.php") + if content, _ := getResourceContent(fileOp, resourcePath); content != "" { + res.Content = content + return res, nil + } + res.Content = string(nginx_conf.IndexPHP) + return res, nil + case "index": + resourcePath := path.Join(defaultPath, "index.html") + if content, _ := getResourceContent(fileOp, resourcePath); content != "" { + res.Content = content + return res, nil + } + res.Content = string(nginx_conf.Index) + return res, nil + case "domain404": + resourcePath := path.Join(rootPath, "404.html") + if content, _ := getResourceContent(fileOp, resourcePath); content != "" { + res.Content = content + return res, nil + } + res.Content = string(nginx_conf.DomainNotFoundHTML) + return res, nil + case "stop": + resourcePath := path.Join(rootPath, "stop", "index.html") + if content, _ := getResourceContent(fileOp, resourcePath); content != "" { + res.Content = content + return res, nil + } + res.Content = string(nginx_conf.StopHTML) + return res, nil + } + return res, nil +} + +func (w WebsiteService) UpdateDefaultHtml(req request.WebsiteHtmlUpdate) error { + nginxInstall, err := getAppInstallByKey(constant.AppOpenresty) + if err != nil { + return err + } + rootPath := path.Join(nginxInstall.GetPath(), "root") + fileOp := files.NewFileOp() + defaultPath := path.Join(rootPath, "default") + if !fileOp.Stat(defaultPath) { + _ = fileOp.CreateDir(defaultPath, 0755) + } + var resourcePath string + switch req.Type { + case "404": + resourcePath = path.Join(defaultPath, "404.html") + case "php": + resourcePath = path.Join(defaultPath, "index.php") + case "index": + resourcePath = path.Join(defaultPath, "index.html") + case "domain404": + resourcePath = path.Join(rootPath, "404.html") + case "stop": + resourcePath = path.Join(rootPath, "stop", "index.html") + default: + return nil + } + return fileOp.SaveFile(resourcePath, req.Content, 0644) +} diff --git a/agent/app/service/website_acme_account.go b/agent/app/service/website_acme_account.go new file mode 100644 index 000000000..18789ddee --- /dev/null +++ b/agent/app/service/website_acme_account.go @@ -0,0 +1,78 @@ +package service + +import ( + "github.com/1Panel-dev/1Panel/agent/app/dto" + "github.com/1Panel-dev/1Panel/agent/app/dto/request" + "github.com/1Panel-dev/1Panel/agent/app/dto/response" + "github.com/1Panel-dev/1Panel/agent/app/model" + "github.com/1Panel-dev/1Panel/agent/buserr" + "github.com/1Panel-dev/1Panel/agent/constant" + "github.com/1Panel-dev/1Panel/agent/utils/ssl" +) + +type WebsiteAcmeAccountService struct { +} + +type IWebsiteAcmeAccountService interface { + Page(search dto.PageInfo) (int64, []response.WebsiteAcmeAccountDTO, error) + Create(create request.WebsiteAcmeAccountCreate) (*response.WebsiteAcmeAccountDTO, error) + Delete(id uint) error +} + +func NewIWebsiteAcmeAccountService() IWebsiteAcmeAccountService { + return &WebsiteAcmeAccountService{} +} + +func (w WebsiteAcmeAccountService) Page(search dto.PageInfo) (int64, []response.WebsiteAcmeAccountDTO, error) { + total, accounts, err := websiteAcmeRepo.Page(search.Page, search.PageSize, commonRepo.WithOrderBy("created_at desc")) + var accountDTOs []response.WebsiteAcmeAccountDTO + for _, account := range accounts { + accountDTOs = append(accountDTOs, response.WebsiteAcmeAccountDTO{ + WebsiteAcmeAccount: account, + }) + } + return total, accountDTOs, err +} + +func (w WebsiteAcmeAccountService) Create(create request.WebsiteAcmeAccountCreate) (*response.WebsiteAcmeAccountDTO, error) { + exist, _ := websiteAcmeRepo.GetFirst(websiteAcmeRepo.WithEmail(create.Email), websiteAcmeRepo.WithType(create.Type)) + if exist != nil { + return nil, buserr.New(constant.ErrEmailIsExist) + } + acmeAccount := &model.WebsiteAcmeAccount{ + Email: create.Email, + Type: create.Type, + KeyType: create.KeyType, + } + + if create.Type == "google" { + if create.EabKid == "" || create.EabHmacKey == "" { + return nil, buserr.New(constant.ErrEabKidOrEabHmacKeyCannotBlank) + } + acmeAccount.EabKid = create.EabKid + acmeAccount.EabHmacKey = create.EabHmacKey + } + + client, err := ssl.NewAcmeClient(acmeAccount) + if err != nil { + return nil, err + } + privateKey, err := ssl.GetPrivateKey(client.User.GetPrivateKey(), ssl.KeyType(create.KeyType)) + if err != nil { + return nil, err + } + acmeAccount.PrivateKey = string(privateKey) + acmeAccount.URL = client.User.Registration.URI + + if err := websiteAcmeRepo.Create(*acmeAccount); err != nil { + return nil, err + } + return &response.WebsiteAcmeAccountDTO{WebsiteAcmeAccount: *acmeAccount}, nil +} + +func (w WebsiteAcmeAccountService) Delete(id uint) error { + if ssls, _ := websiteSSLRepo.List(websiteSSLRepo.WithByAcmeAccountId(id)); len(ssls) > 0 { + return buserr.New(constant.ErrAccountCannotDelete) + } + return websiteAcmeRepo.DeleteBy(commonRepo.WithByID(id)) +} diff --git a/agent/app/service/website_ca.go b/agent/app/service/website_ca.go new file mode 100644 index 000000000..90bcf1251 --- /dev/null +++ b/agent/app/service/website_ca.go @@ -0,0 +1,447 @@ +package service + +import ( + "bytes" + "context" + "crypto/ecdsa" + "crypto/rand" + "crypto/rsa" + "crypto/x509" + "crypto/x509/pkix" + "encoding/pem" + "fmt" + "log" + "math/big" + "net" + "os" + "path" + "strings" + "time" + + "github.com/1Panel-dev/1Panel/agent/app/dto/request" + "github.com/1Panel-dev/1Panel/agent/app/dto/response" + "github.com/1Panel-dev/1Panel/agent/app/model" + "github.com/1Panel-dev/1Panel/agent/buserr" + "github.com/1Panel-dev/1Panel/agent/constant" + "github.com/1Panel-dev/1Panel/agent/global" + "github.com/1Panel-dev/1Panel/agent/i18n" + "github.com/1Panel-dev/1Panel/agent/utils/cmd" + "github.com/1Panel-dev/1Panel/agent/utils/common" + "github.com/1Panel-dev/1Panel/agent/utils/files" + "github.com/1Panel-dev/1Panel/agent/utils/ssl" + "github.com/go-acme/lego/v4/certcrypto" +) + +type WebsiteCAService struct { +} + +type IWebsiteCAService interface { + Page(search request.WebsiteCASearch) (int64, []response.WebsiteCADTO, error) + Create(create request.WebsiteCACreate) (*request.WebsiteCACreate, error) + GetCA(id uint) (*response.WebsiteCADTO, error) + Delete(id uint) error + ObtainSSL(req request.WebsiteCAObtain) (*model.WebsiteSSL, error) + DownloadFile(id uint) (*os.File, error) +} + +func NewIWebsiteCAService() IWebsiteCAService { + return &WebsiteCAService{} +} + +func (w WebsiteCAService) Page(search request.WebsiteCASearch) (int64, []response.WebsiteCADTO, error) { + total, cas, err := websiteCARepo.Page(search.Page, search.PageSize, commonRepo.WithOrderBy("created_at desc")) + if err != nil { + return 0, nil, err + } + var caDTOs []response.WebsiteCADTO + for _, ca := range cas { + caDTOs = append(caDTOs, response.WebsiteCADTO{ + WebsiteCA: ca, + }) + } + return total, caDTOs, err +} + +func (w WebsiteCAService) Create(create request.WebsiteCACreate) (*request.WebsiteCACreate, error) { + if exist, _ := websiteCARepo.GetFirst(commonRepo.WithByName(create.Name)); exist.ID > 0 { + return nil, buserr.New(constant.ErrNameIsExist) + } + + ca := &model.WebsiteCA{ + Name: create.Name, + KeyType: create.KeyType, + } + + pkixName := pkix.Name{ + CommonName: create.CommonName, + Country: []string{create.Country}, + Organization: []string{create.Organization}, + OrganizationalUnit: []string{create.OrganizationUint}, + } + if create.Province != "" { + pkixName.Province = []string{create.Province} + } + if create.City != "" { + pkixName.Locality = []string{create.City} + } + + rootCA := &x509.Certificate{ + SerialNumber: big.NewInt(time.Now().Unix() + 1), + Subject: pkixName, + NotBefore: time.Now(), + NotAfter: time.Now().AddDate(10, 0, 0), + BasicConstraintsValid: true, + IsCA: true, + MaxPathLen: 1, + MaxPathLenZero: false, + KeyUsage: x509.KeyUsageCertSign | x509.KeyUsageCRLSign, + } + + interPrivateKey, interPublicKey, privateBytes, err := createPrivateKey(create.KeyType) + if err != nil { + return nil, err + } + ca.PrivateKey = string(privateBytes) + + rootDer, err := x509.CreateCertificate(rand.Reader, rootCA, rootCA, interPublicKey, interPrivateKey) + if err != nil { + return nil, err + } + rootCert, err := x509.ParseCertificate(rootDer) + if err != nil { + return nil, err + } + certBlock := &pem.Block{ + Type: "CERTIFICATE", + Bytes: rootCert.Raw, + } + pemData := pem.EncodeToMemory(certBlock) + ca.CSR = string(pemData) + + if err := websiteCARepo.Create(context.Background(), ca); err != nil { + return nil, err + } + return &create, nil +} + +func (w WebsiteCAService) GetCA(id uint) (*response.WebsiteCADTO, error) { + res := &response.WebsiteCADTO{} + ca, err := websiteCARepo.GetFirst(commonRepo.WithByID(id)) + if err != nil { + return nil, err + } + res.WebsiteCA = ca + certBlock, _ := pem.Decode([]byte(ca.CSR)) + if certBlock == nil { + return nil, buserr.New("ErrSSLCertificateFormat") + } + cert, err := x509.ParseCertificate(certBlock.Bytes) + if err != nil { + return nil, err + } + res.CommonName = cert.Issuer.CommonName + res.Organization = strings.Join(cert.Issuer.Organization, ",") + res.Country = strings.Join(cert.Issuer.Country, ",") + res.Province = strings.Join(cert.Issuer.Province, ",") + res.City = strings.Join(cert.Issuer.Locality, ",") + res.OrganizationUint = strings.Join(cert.Issuer.OrganizationalUnit, ",") + + return res, nil +} + +func (w WebsiteCAService) Delete(id uint) error { + ssls, _ := websiteSSLRepo.List(websiteSSLRepo.WithByCAID(id)) + if len(ssls) > 0 { + return buserr.New("ErrDeleteCAWithSSL") + } + exist, err := websiteCARepo.GetFirst(commonRepo.WithByID(id)) + if err != nil { + return err + } + if exist.Name == "1Panel" { + return buserr.New("ErrDefaultCA") + } + return websiteCARepo.DeleteBy(commonRepo.WithByID(id)) +} + +func (w WebsiteCAService) ObtainSSL(req request.WebsiteCAObtain) (*model.WebsiteSSL, error) { + var ( + domains []string + ips []net.IP + websiteSSL = &model.WebsiteSSL{} + err error + ca model.WebsiteCA + ) + if req.Renew { + websiteSSL, err = websiteSSLRepo.GetFirst(commonRepo.WithByID(req.SSLID)) + if err != nil { + return nil, err + } + ca, err = websiteCARepo.GetFirst(commonRepo.WithByID(websiteSSL.CaID)) + if err != nil { + return nil, err + } + existDomains := []string{websiteSSL.PrimaryDomain} + if websiteSSL.Domains != "" { + existDomains = append(existDomains, strings.Split(websiteSSL.Domains, ",")...) + } + for _, domain := range existDomains { + if ipAddress := net.ParseIP(domain); ipAddress == nil { + domains = append(domains, domain) + } else { + ips = append(ips, ipAddress) + } + } + } else { + ca, err = websiteCARepo.GetFirst(commonRepo.WithByID(req.ID)) + if err != nil { + return nil, err + } + websiteSSL = &model.WebsiteSSL{ + Provider: constant.SelfSigned, + KeyType: req.KeyType, + PushDir: req.PushDir, + CaID: ca.ID, + AutoRenew: req.AutoRenew, + Description: req.Description, + ExecShell: req.ExecShell, + } + if req.ExecShell { + websiteSSL.Shell = req.Shell + } + if req.PushDir { + if !files.NewFileOp().Stat(req.Dir) { + return nil, buserr.New(constant.ErrLinkPathNotFound) + } + websiteSSL.Dir = req.Dir + } + if req.Domains != "" { + domainArray := strings.Split(req.Domains, "\n") + for _, domain := range domainArray { + if !common.IsValidDomain(domain) { + err = buserr.WithName("ErrDomainFormat", domain) + return nil, err + } else { + if ipAddress := net.ParseIP(domain); ipAddress == nil { + domains = append(domains, domain) + } else { + ips = append(ips, ipAddress) + } + } + } + if len(domains) > 0 { + websiteSSL.PrimaryDomain = domains[0] + websiteSSL.Domains = strings.Join(domains[1:], ",") + } + ipStrings := make([]string, len(ips)) + for i, ip := range ips { + ipStrings[i] = ip.String() + } + if websiteSSL.PrimaryDomain == "" && len(ips) > 0 { + websiteSSL.PrimaryDomain = ipStrings[0] + ipStrings = ipStrings[1:] + } + if len(ipStrings) > 0 { + if websiteSSL.Domains != "" { + websiteSSL.Domains += "," + } + websiteSSL.Domains += strings.Join(ipStrings, ",") + } + + } + } + + rootCertBlock, _ := pem.Decode([]byte(ca.CSR)) + if rootCertBlock == nil { + return nil, buserr.New("ErrSSLCertificateFormat") + } + rootCsr, err := x509.ParseCertificate(rootCertBlock.Bytes) + if err != nil { + return nil, err + } + rootPrivateKeyBlock, _ := pem.Decode([]byte(ca.PrivateKey)) + if rootPrivateKeyBlock == nil { + return nil, buserr.New("ErrSSLCertificateFormat") + } + + var rootPrivateKey any + if ssl.KeyType(ca.KeyType) == certcrypto.EC256 || ssl.KeyType(ca.KeyType) == certcrypto.EC384 { + rootPrivateKey, err = x509.ParseECPrivateKey(rootPrivateKeyBlock.Bytes) + if err != nil { + return nil, err + } + } else { + rootPrivateKey, err = x509.ParsePKCS1PrivateKey(rootPrivateKeyBlock.Bytes) + if err != nil { + return nil, err + } + } + interPrivateKey, interPublicKey, _, err := createPrivateKey(websiteSSL.KeyType) + if err != nil { + return nil, err + } + notAfter := time.Now() + if req.Unit == "year" { + notAfter = notAfter.AddDate(req.Time, 0, 0) + } else { + notAfter = notAfter.AddDate(0, 0, req.Time) + } + interCsr := &x509.Certificate{ + SerialNumber: big.NewInt(time.Now().Unix() + 2), + Subject: rootCsr.Subject, + NotBefore: time.Now(), + NotAfter: notAfter, + BasicConstraintsValid: true, + IsCA: true, + MaxPathLen: 0, + MaxPathLenZero: true, + KeyUsage: x509.KeyUsageCertSign | x509.KeyUsageCRLSign, + } + interDer, err := x509.CreateCertificate(rand.Reader, interCsr, rootCsr, interPublicKey, rootPrivateKey) + if err != nil { + return nil, err + } + interCert, err := x509.ParseCertificate(interDer) + if err != nil { + return nil, err + } + interCertBlock := &pem.Block{ + Type: "CERTIFICATE", + Bytes: interCert.Raw, + } + _, publicKey, privateKeyBytes, err := createPrivateKey(websiteSSL.KeyType) + if err != nil { + return nil, err + } + commonName := "" + if len(domains) > 0 { + commonName = domains[0] + } + if len(ips) > 0 { + commonName = ips[0].String() + } + subject := rootCsr.Subject + subject.CommonName = commonName + csr := &x509.Certificate{ + SerialNumber: big.NewInt(time.Now().Unix() + 3), + Subject: subject, + NotBefore: time.Now(), + NotAfter: notAfter, + BasicConstraintsValid: true, + IsCA: false, + KeyUsage: x509.KeyUsageDigitalSignature | x509.KeyUsageKeyEncipherment, + ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth}, + DNSNames: domains, + IPAddresses: ips, + } + + der, err := x509.CreateCertificate(rand.Reader, csr, interCert, publicKey, interPrivateKey) + if err != nil { + return nil, err + } + cert, err := x509.ParseCertificate(der) + if err != nil { + return nil, err + } + + certBlock := &pem.Block{ + Type: "CERTIFICATE", + Bytes: cert.Raw, + } + websiteSSL.Pem = string(pem.EncodeToMemory(certBlock)) + string(pem.EncodeToMemory(rootCertBlock)) + string(pem.EncodeToMemory(interCertBlock)) + websiteSSL.PrivateKey = string(privateKeyBytes) + websiteSSL.ExpireDate = cert.NotAfter + websiteSSL.StartDate = cert.NotBefore + websiteSSL.Type = cert.Issuer.CommonName + websiteSSL.Organization = rootCsr.Subject.Organization[0] + websiteSSL.Status = constant.SSLReady + + if req.Renew { + if err := websiteSSLRepo.Save(websiteSSL); err != nil { + return nil, err + } + } else { + if err := websiteSSLRepo.Create(context.Background(), websiteSSL); err != nil { + return nil, err + } + } + + logFile, _ := os.OpenFile(path.Join(constant.SSLLogDir, fmt.Sprintf("%s-ssl-%d.log", websiteSSL.PrimaryDomain, websiteSSL.ID)), os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0666) + defer logFile.Close() + logger := log.New(logFile, "", log.LstdFlags) + logger.Println(i18n.GetMsgWithMap("ApplySSLSuccess", map[string]interface{}{"domain": strings.Join(domains, ",")})) + saveCertificateFile(websiteSSL, logger) + if websiteSSL.ExecShell { + workDir := constant.DataDir + if websiteSSL.PushDir { + workDir = websiteSSL.Dir + } + logger.Println(i18n.GetMsgByKey("ExecShellStart")) + if err = cmd.ExecShellWithTimeOut(websiteSSL.Shell, workDir, logger, 30*time.Minute); err != nil { + logger.Println(i18n.GetMsgWithMap("ErrExecShell", map[string]interface{}{"err": err.Error()})) + } else { + logger.Println(i18n.GetMsgByKey("ExecShellSuccess")) + } + } + return websiteSSL, nil +} + +func createPrivateKey(keyType string) (privateKey any, publicKey any, privateKeyBytes []byte, err error) { + privateKey, err = certcrypto.GeneratePrivateKey(ssl.KeyType(keyType)) + if err != nil { + return + } + var ( + caPrivateKeyPEM = new(bytes.Buffer) + ) + if ssl.KeyType(keyType) == certcrypto.EC256 || ssl.KeyType(keyType) == certcrypto.EC384 { + publicKey = &privateKey.(*ecdsa.PrivateKey).PublicKey + publicKey = publicKey.(*ecdsa.PublicKey) + block := &pem.Block{ + Type: "EC PRIVATE KEY", + } + privateBytes, sErr := x509.MarshalECPrivateKey(privateKey.(*ecdsa.PrivateKey)) + if sErr != nil { + err = sErr + return + } + block.Bytes = privateBytes + _ = pem.Encode(caPrivateKeyPEM, block) + } else { + publicKey = &privateKey.(*rsa.PrivateKey).PublicKey + _ = pem.Encode(caPrivateKeyPEM, &pem.Block{ + Type: "RSA PRIVATE KEY", + Bytes: x509.MarshalPKCS1PrivateKey(privateKey.(*rsa.PrivateKey)), + }) + } + privateKeyBytes = caPrivateKeyPEM.Bytes() + return +} + +func (w WebsiteCAService) DownloadFile(id uint) (*os.File, error) { + ca, err := websiteCARepo.GetFirst(commonRepo.WithByID(id)) + if err != nil { + return nil, err + } + fileOp := files.NewFileOp() + dir := path.Join(global.CONF.System.BaseDir, "1panel/tmp/ssl", ca.Name) + if fileOp.Stat(dir) { + if err = fileOp.DeleteDir(dir); err != nil { + return nil, err + } + } + if err = fileOp.CreateDir(dir, 0666); err != nil { + return nil, err + } + if err = fileOp.WriteFile(path.Join(dir, "ca.csr"), strings.NewReader(ca.CSR), 0644); err != nil { + return nil, err + } + if err = fileOp.WriteFile(path.Join(dir, "private.key"), strings.NewReader(ca.PrivateKey), 0644); err != nil { + return nil, err + } + fileName := ca.Name + ".zip" + if err = fileOp.Compress([]string{path.Join(dir, "ca.csr"), path.Join(dir, "private.key")}, dir, fileName, files.SdkZip, ""); err != nil { + return nil, err + } + return os.Open(path.Join(dir, fileName)) +} diff --git a/agent/app/service/website_dns_account.go b/agent/app/service/website_dns_account.go new file mode 100644 index 000000000..e9fdbc49b --- /dev/null +++ b/agent/app/service/website_dns_account.go @@ -0,0 +1,94 @@ +package service + +import ( + "encoding/json" + + "github.com/1Panel-dev/1Panel/agent/app/dto" + "github.com/1Panel-dev/1Panel/agent/app/dto/request" + "github.com/1Panel-dev/1Panel/agent/app/dto/response" + "github.com/1Panel-dev/1Panel/agent/app/model" + "github.com/1Panel-dev/1Panel/agent/buserr" + "github.com/1Panel-dev/1Panel/agent/constant" +) + +type WebsiteDnsAccountService struct { +} + +type IWebsiteDnsAccountService interface { + Page(search dto.PageInfo) (int64, []response.WebsiteDnsAccountDTO, error) + Create(create request.WebsiteDnsAccountCreate) (request.WebsiteDnsAccountCreate, error) + Update(update request.WebsiteDnsAccountUpdate) (request.WebsiteDnsAccountUpdate, error) + Delete(id uint) error +} + +func NewIWebsiteDnsAccountService() IWebsiteDnsAccountService { + return &WebsiteDnsAccountService{} +} + +func (w WebsiteDnsAccountService) Page(search dto.PageInfo) (int64, []response.WebsiteDnsAccountDTO, error) { + total, accounts, err := websiteDnsRepo.Page(search.Page, search.PageSize, commonRepo.WithOrderBy("created_at desc")) + var accountDTOs []response.WebsiteDnsAccountDTO + for _, account := range accounts { + auth := make(map[string]string) + _ = json.Unmarshal([]byte(account.Authorization), &auth) + accountDTOs = append(accountDTOs, response.WebsiteDnsAccountDTO{ + WebsiteDnsAccount: account, + Authorization: auth, + }) + } + return total, accountDTOs, err +} + +func (w WebsiteDnsAccountService) Create(create request.WebsiteDnsAccountCreate) (request.WebsiteDnsAccountCreate, error) { + exist, _ := websiteDnsRepo.GetFirst(commonRepo.WithByName(create.Name)) + if exist != nil { + return request.WebsiteDnsAccountCreate{}, buserr.New(constant.ErrNameIsExist) + } + + authorization, err := json.Marshal(create.Authorization) + if err != nil { + return request.WebsiteDnsAccountCreate{}, err + } + + if err := websiteDnsRepo.Create(model.WebsiteDnsAccount{ + Name: create.Name, + Type: create.Type, + Authorization: string(authorization), + }); err != nil { + return request.WebsiteDnsAccountCreate{}, err + } + + return create, nil +} + +func (w WebsiteDnsAccountService) Update(update request.WebsiteDnsAccountUpdate) (request.WebsiteDnsAccountUpdate, error) { + authorization, err := json.Marshal(update.Authorization) + if err != nil { + return request.WebsiteDnsAccountUpdate{}, err + } + exists, _ := websiteDnsRepo.List(commonRepo.WithByName(update.Name)) + for _, exist := range exists { + if exist.ID != update.ID { + return request.WebsiteDnsAccountUpdate{}, buserr.New(constant.ErrNameIsExist) + } + } + if err := websiteDnsRepo.Save(model.WebsiteDnsAccount{ + BaseModel: model.BaseModel{ + ID: update.ID, + }, + Name: update.Name, + Type: update.Type, + Authorization: string(authorization), + }); err != nil { + return request.WebsiteDnsAccountUpdate{}, err + } + + return update, nil +} + +func (w WebsiteDnsAccountService) Delete(id uint) error { + if ssls, _ := websiteSSLRepo.List(websiteSSLRepo.WithByDnsAccountId(id)); len(ssls) > 0 { + return buserr.New(constant.ErrAccountCannotDelete) + } + return websiteDnsRepo.DeleteBy(commonRepo.WithByID(id)) +} diff --git a/agent/app/service/website_ssl.go b/agent/app/service/website_ssl.go new file mode 100644 index 000000000..e8143e979 --- /dev/null +++ b/agent/app/service/website_ssl.go @@ -0,0 +1,625 @@ +package service + +import ( + "context" + "crypto" + "crypto/x509" + "encoding/pem" + "fmt" + "log" + "os" + "path" + "strconv" + "strings" + "time" + + "github.com/1Panel-dev/1Panel/agent/app/dto/request" + "github.com/1Panel-dev/1Panel/agent/app/dto/response" + "github.com/1Panel-dev/1Panel/agent/app/model" + "github.com/1Panel-dev/1Panel/agent/app/repo" + "github.com/1Panel-dev/1Panel/agent/buserr" + "github.com/1Panel-dev/1Panel/agent/constant" + "github.com/1Panel-dev/1Panel/agent/global" + "github.com/1Panel-dev/1Panel/agent/i18n" + "github.com/1Panel-dev/1Panel/agent/utils/cmd" + "github.com/1Panel-dev/1Panel/agent/utils/common" + "github.com/1Panel-dev/1Panel/agent/utils/files" + "github.com/1Panel-dev/1Panel/agent/utils/ssl" + "github.com/go-acme/lego/v4/certcrypto" + legoLogger "github.com/go-acme/lego/v4/log" + "github.com/jinzhu/gorm" +) + +type WebsiteSSLService struct { +} + +type IWebsiteSSLService interface { + Page(search request.WebsiteSSLSearch) (int64, []response.WebsiteSSLDTO, error) + GetSSL(id uint) (*response.WebsiteSSLDTO, error) + Search(req request.WebsiteSSLSearch) ([]response.WebsiteSSLDTO, error) + Create(create request.WebsiteSSLCreate) (request.WebsiteSSLCreate, error) + GetDNSResolve(req request.WebsiteDNSReq) ([]response.WebsiteDNSRes, error) + GetWebsiteSSL(websiteId uint) (response.WebsiteSSLDTO, error) + Delete(ids []uint) error + Update(update request.WebsiteSSLUpdate) error + Upload(req request.WebsiteSSLUpload) error + ObtainSSL(apply request.WebsiteSSLApply) error + SyncForRestart() error + DownloadFile(id uint) (*os.File, error) +} + +func NewIWebsiteSSLService() IWebsiteSSLService { + return &WebsiteSSLService{} +} + +func (w WebsiteSSLService) Page(search request.WebsiteSSLSearch) (int64, []response.WebsiteSSLDTO, error) { + var ( + result []response.WebsiteSSLDTO + ) + total, sslList, err := websiteSSLRepo.Page(search.Page, search.PageSize, commonRepo.WithOrderBy("created_at desc")) + if err != nil { + return 0, nil, err + } + for _, model := range sslList { + result = append(result, response.WebsiteSSLDTO{ + WebsiteSSL: model, + LogPath: path.Join(constant.SSLLogDir, fmt.Sprintf("%s-ssl-%d.log", model.PrimaryDomain, model.ID)), + }) + } + return total, result, err +} + +func (w WebsiteSSLService) GetSSL(id uint) (*response.WebsiteSSLDTO, error) { + var res response.WebsiteSSLDTO + websiteSSL, err := websiteSSLRepo.GetFirst(commonRepo.WithByID(id)) + if err != nil { + return nil, err + } + res.WebsiteSSL = *websiteSSL + return &res, nil +} + +func (w WebsiteSSLService) Search(search request.WebsiteSSLSearch) ([]response.WebsiteSSLDTO, error) { + var ( + opts []repo.DBOption + result []response.WebsiteSSLDTO + ) + opts = append(opts, commonRepo.WithOrderBy("created_at desc")) + if search.AcmeAccountID != "" { + acmeAccountID, err := strconv.ParseUint(search.AcmeAccountID, 10, 64) + if err != nil { + return nil, err + } + opts = append(opts, websiteSSLRepo.WithByAcmeAccountId(uint(acmeAccountID))) + } + sslList, err := websiteSSLRepo.List(opts...) + if err != nil { + return nil, err + } + for _, sslModel := range sslList { + result = append(result, response.WebsiteSSLDTO{ + WebsiteSSL: sslModel, + }) + } + return result, err +} + +func (w WebsiteSSLService) Create(create request.WebsiteSSLCreate) (request.WebsiteSSLCreate, error) { + if create.Nameserver1 != "" && !common.IsValidIP(create.Nameserver1) { + return create, buserr.New("ErrParseIP") + } + if create.Nameserver2 != "" && !common.IsValidIP(create.Nameserver2) { + return create, buserr.New("ErrParseIP") + } + var res request.WebsiteSSLCreate + acmeAccount, err := websiteAcmeRepo.GetFirst(commonRepo.WithByID(create.AcmeAccountID)) + if err != nil { + return res, err + } + websiteSSL := model.WebsiteSSL{ + Status: constant.SSLInit, + Provider: create.Provider, + AcmeAccountID: acmeAccount.ID, + PrimaryDomain: create.PrimaryDomain, + ExpireDate: time.Now(), + KeyType: create.KeyType, + PushDir: create.PushDir, + Description: create.Description, + Nameserver1: create.Nameserver1, + Nameserver2: create.Nameserver2, + SkipDNS: create.SkipDNS, + DisableCNAME: create.DisableCNAME, + ExecShell: create.ExecShell, + } + if create.ExecShell { + websiteSSL.Shell = create.Shell + } + if create.PushDir { + fileOP := files.NewFileOp() + if !fileOP.Stat(create.Dir) { + _ = fileOP.CreateDir(create.Dir, 0755) + } + websiteSSL.Dir = create.Dir + } + + var domains []string + if create.OtherDomains != "" { + otherDomainArray := strings.Split(create.OtherDomains, "\n") + for _, domain := range otherDomainArray { + if !common.IsValidDomain(domain) { + err = buserr.WithName("ErrDomainFormat", domain) + return res, err + } + domains = append(domains, domain) + } + } + websiteSSL.Domains = strings.Join(domains, ",") + + if create.Provider == constant.DNSAccount || create.Provider == constant.Http { + websiteSSL.AutoRenew = create.AutoRenew + } + if create.Provider == constant.DNSAccount { + dnsAccount, err := websiteDnsRepo.GetFirst(commonRepo.WithByID(create.DnsAccountID)) + if err != nil { + return res, err + } + websiteSSL.DnsAccountID = dnsAccount.ID + } + + if err := websiteSSLRepo.Create(context.TODO(), &websiteSSL); err != nil { + return res, err + } + create.ID = websiteSSL.ID + go func() { + if create.Provider != constant.DnsManual { + if err = w.ObtainSSL(request.WebsiteSSLApply{ + ID: websiteSSL.ID, + }); err != nil { + global.LOG.Errorf("obtain ssl failed, err: %v", err) + } + } + }() + return create, nil +} + +func (w WebsiteSSLService) ObtainSSL(apply request.WebsiteSSLApply) error { + var ( + err error + websiteSSL *model.WebsiteSSL + acmeAccount *model.WebsiteAcmeAccount + dnsAccount *model.WebsiteDnsAccount + ) + + websiteSSL, err = websiteSSLRepo.GetFirst(commonRepo.WithByID(apply.ID)) + if err != nil { + return err + } + acmeAccount, err = websiteAcmeRepo.GetFirst(commonRepo.WithByID(websiteSSL.AcmeAccountID)) + if err != nil { + return err + } + client, err := ssl.NewAcmeClient(acmeAccount) + if err != nil { + return err + } + + switch websiteSSL.Provider { + case constant.DNSAccount: + dnsAccount, err = websiteDnsRepo.GetFirst(commonRepo.WithByID(websiteSSL.DnsAccountID)) + if err != nil { + return err + } + if err = client.UseDns(ssl.DnsType(dnsAccount.Type), dnsAccount.Authorization, *websiteSSL); err != nil { + return err + } + case constant.Http: + appInstall, err := getAppInstallByKey(constant.AppOpenresty) + if err != nil { + if gorm.IsRecordNotFoundError(err) { + return buserr.New("ErrOpenrestyNotFound") + } + return err + } + if err := client.UseHTTP(path.Join(appInstall.GetPath(), "root")); err != nil { + return err + } + case constant.DnsManual: + if err := client.UseManualDns(); err != nil { + return err + } + } + + domains := []string{websiteSSL.PrimaryDomain} + if websiteSSL.Domains != "" { + domains = append(domains, strings.Split(websiteSSL.Domains, ",")...) + } + + var privateKey crypto.PrivateKey + if websiteSSL.PrivateKey == "" { + privateKey, err = certcrypto.GeneratePrivateKey(ssl.KeyType(websiteSSL.KeyType)) + if err != nil { + return err + } + } else { + block, _ := pem.Decode([]byte(websiteSSL.PrivateKey)) + if block == nil { + return buserr.New("invalid PEM block") + } + var privKey crypto.PrivateKey + keyType := ssl.KeyType(websiteSSL.KeyType) + switch keyType { + case certcrypto.EC256, certcrypto.EC384: + privKey, err = x509.ParseECPrivateKey(block.Bytes) + if err != nil { + return nil + } + case certcrypto.RSA2048, certcrypto.RSA3072, certcrypto.RSA4096: + privKey, err = x509.ParsePKCS1PrivateKey(block.Bytes) + if err != nil { + return nil + } + } + privateKey = privKey + } + + websiteSSL.Status = constant.SSLApply + err = websiteSSLRepo.Save(websiteSSL) + if err != nil { + return err + } + + go func() { + logFile, _ := os.OpenFile(path.Join(constant.SSLLogDir, fmt.Sprintf("%s-ssl-%d.log", websiteSSL.PrimaryDomain, websiteSSL.ID)), os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0666) + defer logFile.Close() + logger := log.New(logFile, "", log.LstdFlags) + legoLogger.Logger = logger + startMsg := i18n.GetMsgWithMap("ApplySSLStart", map[string]interface{}{"domain": strings.Join(domains, ","), "type": i18n.GetMsgByKey(websiteSSL.Provider)}) + if websiteSSL.Provider == constant.DNSAccount { + startMsg = startMsg + i18n.GetMsgWithMap("DNSAccountName", map[string]interface{}{"name": dnsAccount.Name, "type": dnsAccount.Type}) + } + legoLogger.Logger.Println(startMsg) + resource, err := client.ObtainSSL(domains, privateKey) + if err != nil { + handleError(websiteSSL, err) + return + } + websiteSSL.PrivateKey = string(resource.PrivateKey) + websiteSSL.Pem = string(resource.Certificate) + websiteSSL.CertURL = resource.CertURL + certBlock, _ := pem.Decode(resource.Certificate) + cert, err := x509.ParseCertificate(certBlock.Bytes) + if err != nil { + handleError(websiteSSL, err) + return + } + websiteSSL.ExpireDate = cert.NotAfter + websiteSSL.StartDate = cert.NotBefore + websiteSSL.Type = cert.Issuer.CommonName + websiteSSL.Organization = cert.Issuer.Organization[0] + websiteSSL.Status = constant.SSLReady + legoLogger.Logger.Println(i18n.GetMsgWithMap("ApplySSLSuccess", map[string]interface{}{"domain": strings.Join(domains, ",")})) + saveCertificateFile(websiteSSL, logger) + + if websiteSSL.ExecShell { + workDir := constant.DataDir + if websiteSSL.PushDir { + workDir = websiteSSL.Dir + } + legoLogger.Logger.Println(i18n.GetMsgByKey("ExecShellStart")) + if err = cmd.ExecShellWithTimeOut(websiteSSL.Shell, workDir, logger, 30*time.Minute); err != nil { + legoLogger.Logger.Println(i18n.GetMsgWithMap("ErrExecShell", map[string]interface{}{"err": err.Error()})) + } else { + legoLogger.Logger.Println(i18n.GetMsgByKey("ExecShellSuccess")) + } + } + + err = websiteSSLRepo.Save(websiteSSL) + if err != nil { + return + } + + websites, _ := websiteRepo.GetBy(websiteRepo.WithWebsiteSSLID(websiteSSL.ID)) + if len(websites) > 0 { + for _, website := range websites { + legoLogger.Logger.Println(i18n.GetMsgWithMap("ApplyWebSiteSSLLog", map[string]interface{}{"name": website.PrimaryDomain})) + if err := createPemFile(website, *websiteSSL); err != nil { + legoLogger.Logger.Println(i18n.GetMsgWithMap("ErrUpdateWebsiteSSL", map[string]interface{}{"name": website.PrimaryDomain, "err": err.Error()})) + } + } + nginxInstall, err := getAppInstallByKey(constant.AppOpenresty) + if err != nil { + return + } + if err := opNginx(nginxInstall.ContainerName, constant.NginxReload); err != nil { + legoLogger.Logger.Println(i18n.GetMsgByKey(constant.ErrSSLApply)) + return + } + legoLogger.Logger.Println(i18n.GetMsgByKey("ApplyWebSiteSSLSuccess")) + } + }() + + return nil +} + +func handleError(websiteSSL *model.WebsiteSSL, err error) { + if websiteSSL.Status == constant.SSLInit || websiteSSL.Status == constant.SSLError { + websiteSSL.Status = constant.Error + } else { + websiteSSL.Status = constant.SSLApplyError + } + websiteSSL.Message = err.Error() + legoLogger.Logger.Println(i18n.GetErrMsg("ApplySSLFailed", map[string]interface{}{"domain": websiteSSL.PrimaryDomain, "detail": err.Error()})) + _ = websiteSSLRepo.Save(websiteSSL) +} + +func (w WebsiteSSLService) GetDNSResolve(req request.WebsiteDNSReq) ([]response.WebsiteDNSRes, error) { + acmeAccount, err := websiteAcmeRepo.GetFirst(commonRepo.WithByID(req.AcmeAccountID)) + if err != nil { + return nil, err + } + + client, err := ssl.NewAcmeClient(acmeAccount) + if err != nil { + return nil, err + } + resolves, err := client.GetDNSResolve(req.Domains) + if err != nil { + return nil, err + } + var res []response.WebsiteDNSRes + for k, v := range resolves { + res = append(res, response.WebsiteDNSRes{ + Domain: k, + Key: v.Key, + Value: v.Value, + Err: v.Err, + }) + } + return res, nil +} + +func (w WebsiteSSLService) GetWebsiteSSL(websiteId uint) (response.WebsiteSSLDTO, error) { + var res response.WebsiteSSLDTO + website, err := websiteRepo.GetFirst(commonRepo.WithByID(websiteId)) + if err != nil { + return res, err + } + websiteSSL, err := websiteSSLRepo.GetFirst(commonRepo.WithByID(website.WebsiteSSLID)) + if err != nil { + return res, err + } + res.WebsiteSSL = *websiteSSL + return res, nil +} + +func (w WebsiteSSLService) Delete(ids []uint) error { + var names []string + for _, id := range ids { + if websites, _ := websiteRepo.GetBy(websiteRepo.WithWebsiteSSLID(id)); len(websites) > 0 { + oldSSL, _ := websiteSSLRepo.GetFirst(commonRepo.WithByID(id)) + if oldSSL.ID > 0 { + names = append(names, oldSSL.PrimaryDomain) + } + continue + } + sslSetting, _ := settingRepo.Get(settingRepo.WithByKey("SSL")) + if sslSetting.Value == "enable" { + sslID, _ := settingRepo.Get(settingRepo.WithByKey("SSLID")) + idValue, _ := strconv.Atoi(sslID.Value) + if idValue > 0 && uint(idValue) == id { + return buserr.New("ErrDeleteWithPanelSSL") + } + } + _ = websiteSSLRepo.DeleteBy(commonRepo.WithByID(id)) + } + if len(names) > 0 { + return buserr.WithName("ErrSSLCannotDelete", strings.Join(names, ",")) + } + return nil +} + +func (w WebsiteSSLService) Update(update request.WebsiteSSLUpdate) error { + websiteSSL, err := websiteSSLRepo.GetFirst(commonRepo.WithByID(update.ID)) + if err != nil { + return err + } + updateParams := make(map[string]interface{}) + updateParams["primary_domain"] = update.PrimaryDomain + updateParams["description"] = update.Description + updateParams["provider"] = update.Provider + updateParams["push_dir"] = update.PushDir + updateParams["disable_cname"] = update.DisableCNAME + updateParams["skip_dns"] = update.SkipDNS + updateParams["nameserver1"] = update.Nameserver1 + updateParams["nameserver2"] = update.Nameserver2 + updateParams["exec_shell"] = update.ExecShell + if update.ExecShell { + updateParams["shell"] = update.Shell + } else { + updateParams["shell"] = "" + } + + if websiteSSL.Provider != constant.SelfSigned { + acmeAccount, err := websiteAcmeRepo.GetFirst(commonRepo.WithByID(update.AcmeAccountID)) + if err != nil { + return err + } + updateParams["acme_account_id"] = acmeAccount.ID + } + + if update.PushDir { + fileOP := files.NewFileOp() + if !fileOP.Stat(update.Dir) { + _ = fileOP.CreateDir(update.Dir, 0755) + } + updateParams["dir"] = update.Dir + } + var domains []string + if update.OtherDomains != "" { + otherDomainArray := strings.Split(update.OtherDomains, "\n") + for _, domain := range otherDomainArray { + if !common.IsValidDomain(domain) { + return buserr.WithName("ErrDomainFormat", domain) + } + domains = append(domains, domain) + } + } + updateParams["domains"] = strings.Join(domains, ",") + if update.Provider == constant.DNSAccount || update.Provider == constant.Http || update.Provider == constant.SelfSigned { + updateParams["auto_renew"] = update.AutoRenew + } else { + updateParams["auto_renew"] = false + } + if update.Provider == constant.DNSAccount { + dnsAccount, err := websiteDnsRepo.GetFirst(commonRepo.WithByID(update.DnsAccountID)) + if err != nil { + return err + } + updateParams["dns_account_id"] = dnsAccount.ID + } + return websiteSSLRepo.SaveByMap(websiteSSL, updateParams) +} + +func (w WebsiteSSLService) Upload(req request.WebsiteSSLUpload) error { + websiteSSL := &model.WebsiteSSL{ + Provider: constant.Manual, + Description: req.Description, + Status: constant.SSLReady, + } + var err error + if req.SSLID > 0 { + websiteSSL, err = websiteSSLRepo.GetFirst(commonRepo.WithByID(req.SSLID)) + if err != nil { + return err + } + websiteSSL.Description = req.Description + } + if req.Type == "local" { + fileOp := files.NewFileOp() + if !fileOp.Stat(req.PrivateKeyPath) { + return buserr.New("ErrSSLKeyNotFound") + } + if !fileOp.Stat(req.CertificatePath) { + return buserr.New("ErrSSLCertificateNotFound") + } + if content, err := fileOp.GetContent(req.PrivateKeyPath); err != nil { + return err + } else { + websiteSSL.PrivateKey = string(content) + } + if content, err := fileOp.GetContent(req.CertificatePath); err != nil { + return err + } else { + websiteSSL.Pem = string(content) + } + } else { + websiteSSL.PrivateKey = req.PrivateKey + websiteSSL.Pem = req.Certificate + } + + privateKeyCertBlock, _ := pem.Decode([]byte(websiteSSL.PrivateKey)) + if privateKeyCertBlock == nil { + return buserr.New("ErrSSLKeyFormat") + } + + var ( + cert *x509.Certificate + pemData = []byte(websiteSSL.Pem) + ) + for { + certBlock, reset := pem.Decode(pemData) + if certBlock == nil { + break + } + cert, err = x509.ParseCertificate(certBlock.Bytes) + if err != nil { + return err + } + if len(cert.DNSNames) > 0 || len(cert.IPAddresses) > 0 { + break + } + pemData = reset + } + if pemData == nil { + return buserr.New("ErrSSLCertificateFormat") + } + + websiteSSL.ExpireDate = cert.NotAfter + websiteSSL.StartDate = cert.NotBefore + websiteSSL.Type = cert.Issuer.CommonName + if len(cert.Issuer.Organization) > 0 { + websiteSSL.Organization = cert.Issuer.Organization[0] + } else { + websiteSSL.Organization = cert.Issuer.CommonName + } + + var domains []string + if len(cert.DNSNames) > 0 { + websiteSSL.PrimaryDomain = cert.DNSNames[0] + domains = cert.DNSNames[1:] + } + if len(cert.IPAddresses) > 0 { + if websiteSSL.PrimaryDomain == "" { + websiteSSL.PrimaryDomain = cert.IPAddresses[0].String() + for _, ip := range cert.IPAddresses[1:] { + domains = append(domains, ip.String()) + } + } else { + for _, ip := range cert.IPAddresses { + domains = append(domains, ip.String()) + } + } + } + websiteSSL.Domains = strings.Join(domains, ",") + + if websiteSSL.ID > 0 { + if err := UpdateSSLConfig(*websiteSSL); err != nil { + return err + } + return websiteSSLRepo.Save(websiteSSL) + } + return websiteSSLRepo.Create(context.Background(), websiteSSL) +} + +func (w WebsiteSSLService) DownloadFile(id uint) (*os.File, error) { + websiteSSL, err := websiteSSLRepo.GetFirst(commonRepo.WithByID(id)) + if err != nil { + return nil, err + } + fileOp := files.NewFileOp() + dir := path.Join(global.CONF.System.BaseDir, "1panel/tmp/ssl", websiteSSL.PrimaryDomain) + if fileOp.Stat(dir) { + if err = fileOp.DeleteDir(dir); err != nil { + return nil, err + } + } + if err = fileOp.CreateDir(dir, 0666); err != nil { + return nil, err + } + if err = fileOp.WriteFile(path.Join(dir, "fullchain.pem"), strings.NewReader(websiteSSL.Pem), 0644); err != nil { + return nil, err + } + if err = fileOp.WriteFile(path.Join(dir, "privkey.pem"), strings.NewReader(websiteSSL.PrivateKey), 0644); err != nil { + return nil, err + } + fileName := websiteSSL.PrimaryDomain + ".zip" + if err = fileOp.Compress([]string{path.Join(dir, "fullchain.pem"), path.Join(dir, "privkey.pem")}, dir, fileName, files.SdkZip, ""); err != nil { + return nil, err + } + return os.Open(path.Join(dir, fileName)) +} + +func (w WebsiteSSLService) SyncForRestart() error { + sslList, err := websiteSSLRepo.List() + if err != nil { + return err + } + for _, ssl := range sslList { + if ssl.Status == constant.SSLApply { + ssl.Status = constant.SystemRestart + ssl.Message = "System restart causing interrupt" + _ = websiteSSLRepo.Save(&ssl) + } + } + return nil +} diff --git a/agent/app/service/website_utils.go b/agent/app/service/website_utils.go new file mode 100644 index 000000000..92525d9a8 --- /dev/null +++ b/agent/app/service/website_utils.go @@ -0,0 +1,1118 @@ +package service + +import ( + "encoding/json" + "fmt" + "log" + "os" + "path" + "path/filepath" + "reflect" + "strconv" + "strings" + "time" + + "github.com/1Panel-dev/1Panel/agent/utils/xpack" + + "github.com/1Panel-dev/1Panel/agent/buserr" + "github.com/1Panel-dev/1Panel/agent/global" + "github.com/1Panel-dev/1Panel/agent/i18n" + "github.com/1Panel-dev/1Panel/agent/utils/cmd" + "github.com/1Panel-dev/1Panel/agent/utils/common" + "github.com/1Panel-dev/1Panel/agent/utils/nginx/components" + "gopkg.in/yaml.v3" + + "github.com/1Panel-dev/1Panel/agent/app/dto/request" + + "github.com/1Panel-dev/1Panel/agent/app/dto" + "github.com/1Panel-dev/1Panel/agent/app/model" + "github.com/1Panel-dev/1Panel/agent/cmd/server/nginx_conf" + "github.com/1Panel-dev/1Panel/agent/constant" + "github.com/1Panel-dev/1Panel/agent/utils/files" + "github.com/1Panel-dev/1Panel/agent/utils/nginx" + "github.com/1Panel-dev/1Panel/agent/utils/nginx/parser" + "github.com/pkg/errors" + "gorm.io/gorm" +) + +func getDomain(domainStr string, defaultPort int) (model.WebsiteDomain, error) { + var ( + err error + domain = model.WebsiteDomain{} + portN int + ) + domainArray := strings.Split(domainStr, ":") + if len(domainArray) == 1 { + domain.Domain, err = handleChineseDomain(domainArray[0]) + if err != nil { + return domain, err + } + domain.Port = defaultPort + return domain, nil + } + if len(domainArray) > 1 { + domain.Domain, err = handleChineseDomain(domainArray[0]) + if err != nil { + return domain, err + } + portStr := domainArray[1] + portN, err = strconv.Atoi(portStr) + if err != nil { + return domain, buserr.WithName("ErrTypePort", portStr) + } + if portN <= 0 || portN > 65535 { + return domain, buserr.New("ErrTypePortRange") + } + domain.Port = portN + return domain, nil + } + return domain, nil +} + +func handleChineseDomain(domain string) (string, error) { + if common.ContainsChinese(domain) { + return common.PunycodeEncode(domain) + } + return domain, nil +} + +func createIndexFile(website *model.Website, runtime *model.Runtime) error { + nginxInstall, err := getAppInstallByKey(constant.AppOpenresty) + if err != nil { + return err + } + var ( + indexPath string + indexContent string + websiteService = NewIWebsiteService() + indexFolder = path.Join(nginxInstall.GetPath(), "www", "sites", website.Alias, "index") + ) + + switch website.Type { + case constant.Static: + indexPath = path.Join(indexFolder, "index.html") + indexHtml, _ := websiteService.GetDefaultHtml("index") + indexContent = indexHtml.Content + case constant.Runtime: + if runtime.Type == constant.RuntimePHP { + indexPath = path.Join(indexFolder, "index.php") + indexPhp, _ := websiteService.GetDefaultHtml("php") + indexContent = indexPhp.Content + } + } + + fileOp := files.NewFileOp() + if !fileOp.Stat(indexFolder) { + if err := fileOp.CreateDir(indexFolder, 0755); err != nil { + return err + } + } + if !fileOp.Stat(indexPath) { + if err := fileOp.CreateFile(indexPath); err != nil { + return err + } + } + if website.Type == constant.Runtime && runtime.Resource == constant.ResourceAppstore { + if err := chownRootDir(indexFolder); err != nil { + return err + } + } + if err := fileOp.WriteFile(indexPath, strings.NewReader(indexContent), 0755); err != nil { + return err + } + + html404, _ := websiteService.GetDefaultHtml("404") + path404 := path.Join(indexFolder, "404.html") + if err := fileOp.WriteFile(path404, strings.NewReader(html404.Content), 0755); err != nil { + return err + } + + return nil +} + +func createProxyFile(website *model.Website) error { + nginxInstall, err := getAppInstallByKey(constant.AppOpenresty) + if err != nil { + return err + } + proxyFolder := path.Join(constant.AppInstallDir, constant.AppOpenresty, nginxInstall.Name, "www", "sites", website.Alias, "proxy") + filePath := path.Join(proxyFolder, "root.conf") + fileOp := files.NewFileOp() + if !fileOp.Stat(proxyFolder) { + if err := fileOp.CreateDir(proxyFolder, 0755); err != nil { + return err + } + } + if !fileOp.Stat(filePath) { + if err := fileOp.CreateFile(filePath); err != nil { + return err + } + } + config, err := parser.NewStringParser(string(nginx_conf.Proxy)).Parse() + if err != nil { + return err + } + config.FilePath = filePath + directives := config.Directives + location, ok := directives[0].(*components.Location) + if !ok { + return errors.New("error") + } + location.ChangePath("^~", "/") + location.UpdateDirective("proxy_pass", []string{website.Proxy}) + location.UpdateDirective("proxy_set_header", []string{"Host", "$host"}) + if err := nginx.WriteConfig(config, nginx.IndentedStyle); err != nil { + return buserr.WithErr(constant.ErrUpdateBuWebsite, err) + } + return nil +} + +func createWebsiteFolder(nginxInstall model.AppInstall, website *model.Website, runtime *model.Runtime) error { + nginxFolder := path.Join(constant.AppInstallDir, constant.AppOpenresty, nginxInstall.Name) + siteFolder := path.Join(nginxFolder, "www", "sites", website.Alias) + fileOp := files.NewFileOp() + if !fileOp.Stat(siteFolder) { + if err := fileOp.CreateDir(siteFolder, 0755); err != nil { + return err + } + if err := fileOp.CreateDir(path.Join(siteFolder, "log"), 0755); err != nil { + return err + } + if err := fileOp.CreateFile(path.Join(siteFolder, "log", "access.log")); err != nil { + return err + } + if err := fileOp.CreateFile(path.Join(siteFolder, "log", "error.log")); err != nil { + return err + } + if err := fileOp.CreateDir(path.Join(siteFolder, "index"), 0775); err != nil { + return err + } + if err := fileOp.CreateDir(path.Join(siteFolder, "ssl"), 0755); err != nil { + return err + } + if website.Type == constant.Runtime { + if runtime.Type == constant.RuntimePHP && runtime.Resource == constant.ResourceLocal { + phpPoolDir := path.Join(siteFolder, "php-pool") + if err := fileOp.CreateDir(phpPoolDir, 0755); err != nil { + return err + } + if err := fileOp.CreateFile(path.Join(phpPoolDir, "php-fpm.sock")); err != nil { + return err + } + } + } + if website.Type == constant.Static || (website.Type == constant.Runtime && runtime.Type == constant.RuntimePHP) { + if err := createIndexFile(website, runtime); err != nil { + return err + } + } + if website.Type == constant.Proxy { + if err := createProxyFile(website); err != nil { + return err + } + } + } + return nil +} + +func configDefaultNginx(website *model.Website, domains []model.WebsiteDomain, appInstall *model.AppInstall, runtime *model.Runtime) error { + nginxInstall, err := getAppInstallByKey(constant.AppOpenresty) + if err != nil { + return err + } + if err := createWebsiteFolder(nginxInstall, website, runtime); err != nil { + return err + } + + nginxFileName := website.Alias + ".conf" + configPath := path.Join(constant.AppInstallDir, constant.AppOpenresty, nginxInstall.Name, "conf", "conf.d", nginxFileName) + nginxContent := string(nginx_conf.WebsiteDefault) + config, err := parser.NewStringParser(nginxContent).Parse() + if err != nil { + return err + } + servers := config.FindServers() + if len(servers) == 0 { + return errors.New("nginx config is not valid") + } + server := servers[0] + server.DeleteListen("80") + var serverNames []string + for _, domain := range domains { + serverNames = append(serverNames, domain.Domain) + server.UpdateListen(strconv.Itoa(domain.Port), false) + if website.IPV6 { + server.UpdateListen("[::]:"+strconv.Itoa(domain.Port), false) + } + } + server.UpdateServerName(serverNames) + + siteFolder := path.Join("/www", "sites", website.Alias) + server.UpdateDirective("access_log", []string{path.Join(siteFolder, "log", "access.log"), "main"}) + server.UpdateDirective("error_log", []string{path.Join(siteFolder, "log", "error.log")}) + + rootIndex := path.Join("/www/sites", website.Alias, "index") + switch website.Type { + case constant.Deployment: + proxy := fmt.Sprintf("http://127.0.0.1:%d", appInstall.HttpPort) + server.UpdateRootProxy([]string{proxy}) + case constant.Static: + server.UpdateRoot(rootIndex) + server.UpdateDirective("error_page", []string{"404", "/404.html"}) + case constant.Proxy: + nginxInclude := fmt.Sprintf("/www/sites/%s/proxy/*.conf", website.Alias) + server.UpdateDirective("include", []string{nginxInclude}) + case constant.Runtime: + switch runtime.Type { + case constant.RuntimePHP: + server.UpdateDirective("error_page", []string{"404", "/404.html"}) + if runtime.Resource == constant.ResourceLocal { + switch runtime.Type { + case constant.RuntimePHP: + server.UpdateRoot(rootIndex) + localPath := path.Join(nginxInstall.GetPath(), rootIndex, "index.php") + server.UpdatePHPProxy([]string{website.Proxy}, localPath) + } + } else { + server.UpdateRoot(rootIndex) + server.UpdatePHPProxy([]string{website.Proxy}, "") + } + case constant.RuntimeNode, constant.RuntimeJava, constant.RuntimeGo: + proxy := fmt.Sprintf("http://127.0.0.1:%d", runtime.Port) + server.UpdateRootProxy([]string{proxy}) + } + } + + config.FilePath = configPath + if err := nginx.WriteConfig(config, nginx.IndentedStyle); err != nil { + return err + } + if err := opNginx(nginxInstall.ContainerName, constant.NginxCheck); err != nil { + _ = deleteWebsiteFolder(nginxInstall, website) + return err + } + if err := opNginx(nginxInstall.ContainerName, constant.NginxReload); err != nil { + _ = deleteWebsiteFolder(nginxInstall, website) + return err + } + return nil +} + +func createWafConfig(website *model.Website, domains []model.WebsiteDomain) error { + nginxInstall, err := getAppInstallByKey(constant.AppOpenresty) + if err != nil { + return err + } + + if !common.CompareVersion(nginxInstall.Version, "1.21.4.3-2-0") { + return nil + } + wafDataPath := path.Join(nginxInstall.GetPath(), "1pwaf", "data") + fileOp := files.NewFileOp() + if !fileOp.Stat(wafDataPath) { + return nil + } + websitesConfigPath := path.Join(wafDataPath, "conf", "sites.json") + content, err := fileOp.GetContent(websitesConfigPath) + if err != nil { + return err + } + var websitesArray []request.WafWebsite + if len(content) != 0 { + if err := json.Unmarshal(content, &websitesArray); err != nil { + return err + } + } + wafWebsite := request.WafWebsite{ + Key: website.Alias, + Domains: make([]string, 0), + Host: make([]string, 0), + } + + for _, domain := range domains { + wafWebsite.Domains = append(wafWebsite.Domains, domain.Domain) + wafWebsite.Host = append(wafWebsite.Host, domain.Domain+":"+strconv.Itoa(domain.Port)) + } + websitesArray = append(websitesArray, wafWebsite) + websitesContent, err := json.Marshal(websitesArray) + if err != nil { + return err + } + if err := fileOp.SaveFileWithByte(websitesConfigPath, websitesContent, 0644); err != nil { + return err + } + + var ( + sitesDir = path.Join(wafDataPath, "sites") + defaultConfigPath = path.Join(wafDataPath, "conf", "siteConfig.json") + defaultRuleDir = path.Join(wafDataPath, "rules") + websiteDir = path.Join(sitesDir, website.Alias) + ) + + defaultConfigContent, err := fileOp.GetContent(defaultConfigPath) + if err != nil { + return err + } + + if !fileOp.Stat(websiteDir) { + if err = fileOp.CreateDir(websiteDir, 0755); err != nil { + return err + } + } + defer func() { + if err != nil { + _ = fileOp.DeleteDir(websiteDir) + } + }() + + if err = fileOp.SaveFileWithByte(path.Join(websiteDir, "config.json"), defaultConfigContent, 0644); err != nil { + return err + } + + websiteRuleDir := path.Join(websiteDir, "rules") + if !fileOp.Stat(websiteRuleDir) { + if err := fileOp.CreateDir(websiteRuleDir, 0755); err != nil { + return err + } + } + defaultRulesName := []string{"acl", "args", "cookie", "defaultUaBlack", "defaultUrlBlack", "fileExt", "header", "methodWhite", "cdn"} + for _, ruleName := range defaultRulesName { + srcPath := path.Join(defaultRuleDir, ruleName+".json") + if fileOp.Stat(srcPath) { + _ = fileOp.Copy(srcPath, websiteRuleDir) + } + } + + if err = opNginx(nginxInstall.ContainerName, constant.NginxCheck); err != nil { + return err + } + if err = opNginx(nginxInstall.ContainerName, constant.NginxReload); err != nil { + return err + } + + return nil +} + +func delNginxConfig(website model.Website, force bool) error { + nginxApp, err := appRepo.GetFirst(appRepo.WithKey(constant.AppOpenresty)) + if err != nil { + return err + } + nginxInstall, err := appInstallRepo.GetFirst(appInstallRepo.WithAppId(nginxApp.ID)) + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return nil + } + return err + } + + nginxFileName := website.Alias + ".conf" + configPath := path.Join(constant.AppInstallDir, constant.AppOpenresty, nginxInstall.Name, "conf", "conf.d", nginxFileName) + fileOp := files.NewFileOp() + + if !fileOp.Stat(configPath) { + return nil + } + if err := fileOp.DeleteFile(configPath); err != nil { + return err + } + sitePath := path.Join(constant.AppInstallDir, constant.AppOpenresty, nginxInstall.Name, "www", "sites", website.Alias) + if fileOp.Stat(sitePath) { + xpack.RemoveTamper(website.Alias) + _ = fileOp.DeleteDir(sitePath) + } + + if err := opNginx(nginxInstall.ContainerName, constant.NginxReload); err != nil { + if force { + return nil + } + return err + } + return nil +} + +func delWafConfig(website model.Website, force bool) error { + nginxInstall, err := getAppInstallByKey(constant.AppOpenresty) + if err != nil { + return err + } + if !common.CompareVersion(nginxInstall.Version, "1.21.4.3-2-0") { + return nil + } + wafDataPath := path.Join(nginxInstall.GetPath(), "1pwaf", "data") + fileOp := files.NewFileOp() + if !fileOp.Stat(wafDataPath) { + return nil + } + monitorDir := path.Join(wafDataPath, "db", "sites", website.Alias) + if fileOp.Stat(monitorDir) { + _ = fileOp.DeleteDir(monitorDir) + } + websitesConfigPath := path.Join(wafDataPath, "conf", "sites.json") + content, err := fileOp.GetContent(websitesConfigPath) + if err != nil { + return err + } + var websitesArray []request.WafWebsite + var newWebsiteArray []request.WafWebsite + if len(content) > 0 { + if err = json.Unmarshal(content, &websitesArray); err != nil { + return err + } + } + for _, wafWebsite := range websitesArray { + if wafWebsite.Key != website.Alias { + newWebsiteArray = append(newWebsiteArray, wafWebsite) + } + } + websitesContent, err := json.Marshal(newWebsiteArray) + if err != nil { + return err + } + if err := fileOp.SaveFileWithByte(websitesConfigPath, websitesContent, 0644); err != nil { + return err + } + + _ = fileOp.DeleteDir(path.Join(wafDataPath, "sites", website.Alias)) + + if err := opNginx(nginxInstall.ContainerName, constant.NginxReload); err != nil { + if force { + return nil + } + return err + } + return nil +} + +func addListenAndServerName(website model.Website, ports []int, domains []string) error { + nginxFull, err := getNginxFull(&website) + if err != nil { + return nil + } + nginxConfig := nginxFull.SiteConfig + config := nginxFull.SiteConfig.Config + server := config.FindServers()[0] + for _, port := range ports { + server.AddListen(strconv.Itoa(port), false) + if website.IPV6 { + server.UpdateListen("[::]:"+strconv.Itoa(port), false) + } + } + for _, domain := range domains { + server.AddServerName(domain) + } + if err := nginx.WriteConfig(config, nginx.IndentedStyle); err != nil { + return err + } + return nginxCheckAndReload(nginxConfig.OldContent, nginxConfig.FilePath, nginxFull.Install.ContainerName) +} + +func deleteListenAndServerName(website model.Website, binds []string, domains []string) error { + nginxFull, err := getNginxFull(&website) + if err != nil { + return nil + } + nginxConfig := nginxFull.SiteConfig + config := nginxFull.SiteConfig.Config + server := config.FindServers()[0] + for _, bind := range binds { + server.DeleteListen(bind) + server.DeleteListen("[::]:" + bind) + } + for _, domain := range domains { + server.DeleteServerName(domain) + } + + if err := nginx.WriteConfig(config, nginx.IndentedStyle); err != nil { + return err + } + return nginxCheckAndReload(nginxConfig.OldContent, nginxConfig.FilePath, nginxFull.Install.ContainerName) +} + +func createPemFile(website model.Website, websiteSSL model.WebsiteSSL) error { + nginxApp, err := appRepo.GetFirst(appRepo.WithKey(constant.AppOpenresty)) + if err != nil { + return err + } + nginxInstall, err := appInstallRepo.GetFirst(appInstallRepo.WithAppId(nginxApp.ID)) + if err != nil { + return err + } + + configDir := path.Join(constant.AppInstallDir, constant.AppOpenresty, nginxInstall.Name, "www", "sites", website.Alias, "ssl") + fileOp := files.NewFileOp() + + if !fileOp.Stat(configDir) { + if err := fileOp.CreateDir(configDir, 0775); err != nil { + return err + } + } + + fullChainFile := path.Join(configDir, "fullchain.pem") + privatePemFile := path.Join(configDir, "privkey.pem") + + if !fileOp.Stat(fullChainFile) { + if err := fileOp.CreateFile(fullChainFile); err != nil { + return err + } + } + if !fileOp.Stat(privatePemFile) { + if err := fileOp.CreateFile(privatePemFile); err != nil { + return err + } + } + + if err := fileOp.WriteFile(fullChainFile, strings.NewReader(websiteSSL.Pem), 0644); err != nil { + return err + } + if err := fileOp.WriteFile(privatePemFile, strings.NewReader(websiteSSL.PrivateKey), 0644); err != nil { + return err + } + return nil +} + +func applySSL(website model.Website, websiteSSL model.WebsiteSSL, req request.WebsiteHTTPSOp) error { + nginxFull, err := getNginxFull(&website) + if err != nil { + return nil + } + domains, err := websiteDomainRepo.GetBy(websiteDomainRepo.WithWebsiteId(website.ID)) + if err != nil { + return nil + } + noDefaultPort := true + for _, domain := range domains { + if domain.Port == 80 { + noDefaultPort = false + } + } + config := nginxFull.SiteConfig.Config + server := config.FindServers()[0] + + httpPort := strconv.Itoa(nginxFull.Install.HttpPort) + httpsPort := strconv.Itoa(nginxFull.Install.HttpsPort) + httpPortIPV6 := "[::]:" + httpPort + httpsPortIPV6 := "[::]:" + httpsPort + + server.UpdateListen(httpsPort, website.DefaultServer, "ssl", "http2") + if website.IPV6 { + server.UpdateListen(httpsPortIPV6, website.DefaultServer, "ssl", "http2") + } + + switch req.HttpConfig { + case constant.HTTPSOnly: + server.RemoveListenByBind(httpPort) + server.RemoveListenByBind(httpPortIPV6) + server.RemoveDirective("if", []string{"($scheme"}) + case constant.HTTPToHTTPS: + if !noDefaultPort { + server.UpdateListen(httpPort, website.DefaultServer) + } + if website.IPV6 { + server.UpdateListen(httpPortIPV6, website.DefaultServer) + } + server.AddHTTP2HTTPS() + case constant.HTTPAlso: + if !noDefaultPort { + server.UpdateListen(httpPort, website.DefaultServer) + } + server.RemoveDirective("if", []string{"($scheme"}) + if website.IPV6 { + server.UpdateListen(httpPortIPV6, website.DefaultServer) + } + } + + if !req.Hsts { + server.RemoveDirective("add_header", []string{"Strict-Transport-Security", "\"max-age=31536000\""}) + } + + if err := nginx.WriteConfig(config, nginx.IndentedStyle); err != nil { + return err + } + if err := createPemFile(website, websiteSSL); err != nil { + return err + } + nginxParams := getNginxParamsFromStaticFile(dto.SSL, []dto.NginxParam{}) + for i, param := range nginxParams { + if param.Name == "ssl_certificate" { + nginxParams[i].Params = []string{path.Join("/www", "sites", website.Alias, "ssl", "fullchain.pem")} + } + if param.Name == "ssl_certificate_key" { + nginxParams[i].Params = []string{path.Join("/www", "sites", website.Alias, "ssl", "privkey.pem")} + } + if param.Name == "ssl_protocols" { + nginxParams[i].Params = req.SSLProtocol + } + if param.Name == "ssl_ciphers" { + nginxParams[i].Params = []string{req.Algorithm} + } + } + if req.Hsts { + nginxParams = append(nginxParams, dto.NginxParam{ + Name: "add_header", + Params: []string{"Strict-Transport-Security", "\"max-age=31536000\""}, + }) + } + + if err := updateNginxConfig(constant.NginxScopeServer, nginxParams, &website); err != nil { + return err + } + return nil +} + +func getParamArray(key string, param interface{}) []string { + var res []string + switch p := param.(type) { + case string: + if key == "index" { + res = strings.Split(p, "\n") + return res + } + + res = strings.Split(p, " ") + return res + } + return res +} + +func handleParamMap(paramMap map[string]string, keys []string) []dto.NginxParam { + var nginxParams []dto.NginxParam + for k, v := range paramMap { + for _, name := range keys { + if name == k { + param := dto.NginxParam{ + Name: k, + Params: getParamArray(k, v), + } + nginxParams = append(nginxParams, param) + } + } + } + return nginxParams +} + +func getNginxParams(params interface{}, keys []string) []dto.NginxParam { + var nginxParams []dto.NginxParam + + switch p := params.(type) { + case map[string]interface{}: + return handleParamMap(toMapStr(p), keys) + case []interface{}: + for _, mA := range p { + if m, ok := mA.(map[string]interface{}); ok { + nginxParams = append(nginxParams, handleParamMap(toMapStr(m), keys)...) + } + } + } + return nginxParams +} + +func toMapStr(m map[string]interface{}) map[string]string { + ret := make(map[string]string, len(m)) + for k, v := range m { + ret[k] = fmt.Sprint(v) + } + return ret +} + +func deleteWebsiteFolder(nginxInstall model.AppInstall, website *model.Website) error { + nginxFolder := path.Join(constant.AppInstallDir, constant.AppOpenresty, nginxInstall.Name) + siteFolder := path.Join(nginxFolder, "www", "sites", website.Alias) + fileOp := files.NewFileOp() + if fileOp.Stat(siteFolder) { + _ = fileOp.DeleteDir(siteFolder) + } + nginxFilePath := path.Join(nginxFolder, "conf", "conf.d", website.PrimaryDomain+".conf") + if fileOp.Stat(nginxFilePath) { + _ = fileOp.DeleteFile(nginxFilePath) + } + return nil +} + +func opWebsite(website *model.Website, operate string) error { + nginxInstall, err := getNginxFull(website) + if err != nil { + return err + } + config := nginxInstall.SiteConfig.Config + servers := config.FindServers() + if len(servers) == 0 { + return errors.New("nginx config is not valid") + } + server := servers[0] + if operate == constant.StopWeb { + proxyInclude := fmt.Sprintf("/www/sites/%s/proxy/*.conf", website.Alias) + server.RemoveDirective("include", []string{proxyInclude}) + rewriteInclude := fmt.Sprintf("/www/sites/%s/rewrite/%s.conf", website.Alias, website.Alias) + server.RemoveDirective("include", []string{rewriteInclude}) + + switch website.Type { + case constant.Deployment: + server.RemoveDirective("location", []string{"/"}) + case constant.Runtime: + runtime, err := runtimeRepo.GetFirst(commonRepo.WithByID(website.RuntimeID)) + if err != nil { + return err + } + if runtime.Type == constant.RuntimePHP { + server.RemoveDirective("location", []string{"~", "[^/]\\.php(/|$)"}) + } else { + server.RemoveDirective("location", []string{"/"}) + } + } + server.UpdateRoot("/usr/share/nginx/html/stop") + website.Status = constant.WebStopped + } + if operate == constant.StartWeb { + absoluteIncludeDir := path.Join(nginxInstall.Install.GetPath(), fmt.Sprintf("/www/sites/%s/proxy", website.Alias)) + fileOp := files.NewFileOp() + if fileOp.Stat(absoluteIncludeDir) && !files.IsEmptyDir(absoluteIncludeDir) { + proxyInclude := fmt.Sprintf("/www/sites/%s/proxy/*.conf", website.Alias) + server.UpdateDirective("include", []string{proxyInclude}) + } + rewriteInclude := fmt.Sprintf("/www/sites/%s/rewrite/%s.conf", website.Alias, website.Alias) + absoluteRewritePath := path.Join(nginxInstall.Install.GetPath(), rewriteInclude) + if fileOp.Stat(absoluteRewritePath) { + server.UpdateDirective("include", []string{rewriteInclude}) + } + rootIndex := path.Join("/www/sites", website.Alias, "index") + if website.SiteDir != "/" { + rootIndex = path.Join(rootIndex, website.SiteDir) + } + switch website.Type { + case constant.Deployment: + server.RemoveDirective("root", nil) + appInstall, err := appInstallRepo.GetFirst(commonRepo.WithByID(website.AppInstallID)) + if err != nil { + return err + } + proxy := fmt.Sprintf("http://127.0.0.1:%d", appInstall.HttpPort) + server.UpdateRootProxy([]string{proxy}) + case constant.Static: + server.UpdateRoot(rootIndex) + server.UpdateRootLocation() + case constant.Proxy: + server.RemoveDirective("root", nil) + case constant.Runtime: + server.UpdateRoot(rootIndex) + localPath := "" + runtime, err := runtimeRepo.GetFirst(commonRepo.WithByID(website.RuntimeID)) + if err != nil { + return err + } + if runtime.Type == constant.RuntimePHP { + if website.ProxyType == constant.RuntimeProxyUnix || website.ProxyType == constant.RuntimeProxyTcp { + localPath = path.Join(nginxInstall.Install.GetPath(), rootIndex, "index.php") + } + server.UpdatePHPProxy([]string{website.Proxy}, localPath) + } else { + proxy := fmt.Sprintf("http://127.0.0.1:%d", runtime.Port) + server.UpdateRootProxy([]string{proxy}) + } + } + website.Status = constant.WebRunning + now := time.Now() + if website.ExpireDate.Before(now) { + defaultDate, _ := time.Parse(constant.DateLayout, constant.DefaultDate) + website.ExpireDate = defaultDate + } + } + + if err := nginx.WriteConfig(config, nginx.IndentedStyle); err != nil { + return err + } + return nginxCheckAndReload(nginxInstall.SiteConfig.OldContent, config.FilePath, nginxInstall.Install.ContainerName) +} + +func changeIPV6(website model.Website, enable bool) error { + nginxFull, err := getNginxFull(&website) + if err != nil { + return nil + } + config := nginxFull.SiteConfig.Config + server := config.FindServers()[0] + listens := server.Listens + if enable { + for _, listen := range listens { + if strings.HasPrefix(listen.Bind, "[::]:") { + continue + } + exist := false + ipv6Bind := fmt.Sprintf("[::]:%s", listen.Bind) + for _, li := range listens { + if li.Bind == ipv6Bind { + exist = true + break + } + } + if !exist { + server.UpdateListen(ipv6Bind, false, listen.GetParameters()[1:]...) + } + } + } else { + for _, listen := range listens { + if strings.HasPrefix(listen.Bind, "[::]:") { + server.RemoveListenByBind(listen.Bind) + } + } + } + if err := nginx.WriteConfig(config, nginx.IndentedStyle); err != nil { + return err + } + return nginxCheckAndReload(nginxFull.SiteConfig.OldContent, config.FilePath, nginxFull.Install.ContainerName) +} + +func checkIsLinkApp(website model.Website) bool { + if website.Type == constant.Deployment { + return true + } + if website.Type == constant.Runtime { + runtime, _ := runtimeRepo.GetFirst(commonRepo.WithByID(website.RuntimeID)) + return runtime.Resource == constant.ResourceAppstore + } + return false +} + +func chownRootDir(path string) error { + _, err := cmd.ExecWithTimeOut(fmt.Sprintf(`chown -R 1000:1000 "%s"`, path), 1*time.Second) + if err != nil { + return err + } + return nil +} + +func changeServiceName(newComposeContent, newServiceName string) (composeByte []byte, err error) { + composeMap := make(map[string]interface{}) + if err = yaml.Unmarshal([]byte(newComposeContent), &composeMap); err != nil { + return + } + value, ok := composeMap["services"] + if !ok || value == nil { + err = buserr.New(constant.ErrFileParse) + return + } + servicesMap := value.(map[string]interface{}) + + index := 0 + serviceName := "" + for k := range servicesMap { + serviceName = k + if index > 0 { + continue + } + index++ + } + if newServiceName != serviceName { + servicesMap[newServiceName] = servicesMap[serviceName] + delete(servicesMap, serviceName) + } + + return yaml.Marshal(composeMap) +} + +func getWebsiteDomains(domains string, defaultPort int, websiteID uint) (domainModels []model.WebsiteDomain, addPorts []int, addDomains []string, err error) { + var ( + ports = make(map[int]struct{}) + ) + domainArray := strings.Split(domains, "\n") + for _, domain := range domainArray { + if domain == "" { + continue + } + if !common.IsValidDomain(domain) { + err = buserr.WithName("ErrDomainFormat", domain) + return + } + var domainModel model.WebsiteDomain + domainModel, err = getDomain(domain, defaultPort) + if err != nil { + return + } + if reflect.DeepEqual(domainModel, model.WebsiteDomain{}) { + continue + } + domainModel.WebsiteID = websiteID + domainModels = append(domainModels, domainModel) + if domainModel.Port != defaultPort { + ports[domainModel.Port] = struct{}{} + } + if exist, _ := websiteDomainRepo.GetFirst(websiteDomainRepo.WithDomain(domainModel.Domain), websiteDomainRepo.WithWebsiteId(websiteID)); exist.ID == 0 { + addDomains = append(addDomains, domainModel.Domain) + } + } + for _, domain := range domainModels { + if exist, _ := websiteDomainRepo.GetFirst(websiteDomainRepo.WithDomain(domain.Domain), websiteDomainRepo.WithPort(domain.Port)); exist.ID > 0 { + website, _ := websiteRepo.GetFirst(commonRepo.WithByID(exist.WebsiteID)) + err = buserr.WithName(constant.ErrDomainIsUsed, website.PrimaryDomain) + return + } + } + + for port := range ports { + if existPorts, _ := websiteDomainRepo.GetBy(websiteDomainRepo.WithPort(port)); len(existPorts) == 0 { + errMap := make(map[string]interface{}) + errMap["port"] = port + appInstall, _ := appInstallRepo.GetFirst(appInstallRepo.WithPort(port)) + if appInstall.ID > 0 { + errMap["type"] = i18n.GetMsgByKey("TYPE_APP") + errMap["name"] = appInstall.Name + err = buserr.WithMap("ErrPortExist", errMap, nil) + return + } + runtime, _ := runtimeRepo.GetFirst(runtimeRepo.WithPort(port)) + if runtime != nil { + errMap["type"] = i18n.GetMsgByKey("TYPE_RUNTIME") + errMap["name"] = runtime.Name + err = buserr.WithMap("ErrPortExist", errMap, nil) + return + } + if common.ScanPort(port) { + err = buserr.WithDetail(constant.ErrPortInUsed, port, nil) + return + } + } + if existPorts, _ := websiteDomainRepo.GetBy(websiteDomainRepo.WithWebsiteId(websiteID), websiteDomainRepo.WithPort(port)); len(existPorts) == 0 { + addPorts = append(addPorts, port) + } + } + + return +} + +func saveCertificateFile(websiteSSL *model.WebsiteSSL, logger *log.Logger) { + if websiteSSL.PushDir { + fileOp := files.NewFileOp() + var ( + pushErr error + MsgMap = map[string]interface{}{"path": websiteSSL.Dir, "status": i18n.GetMsgByKey("Success")} + ) + if pushErr = fileOp.SaveFile(path.Join(websiteSSL.Dir, "privkey.pem"), websiteSSL.PrivateKey, 0666); pushErr != nil { + MsgMap["status"] = i18n.GetMsgByKey("Failed") + logger.Println(i18n.GetMsgWithMap("PushDirLog", MsgMap)) + logger.Println("Push dir failed:" + pushErr.Error()) + } + if pushErr = fileOp.SaveFile(path.Join(websiteSSL.Dir, "fullchain.pem"), websiteSSL.Pem, 0666); pushErr != nil { + MsgMap["status"] = i18n.GetMsgByKey("Failed") + logger.Println(i18n.GetMsgWithMap("PushDirLog", MsgMap)) + logger.Println("Push dir failed:" + pushErr.Error()) + } + if pushErr == nil { + logger.Println(i18n.GetMsgWithMap("PushDirLog", MsgMap)) + } + } +} + +func GetSystemSSL() (bool, uint) { + sslSetting, err := settingRepo.Get(settingRepo.WithByKey("SSL")) + if err != nil { + global.LOG.Errorf("load service ssl from setting failed, err: %v", err) + return false, 0 + } + if sslSetting.Value == "enable" { + sslID, _ := settingRepo.Get(settingRepo.WithByKey("SSLID")) + idValue, _ := strconv.Atoi(sslID.Value) + if idValue > 0 { + return true, uint(idValue) + } + } + return false, 0 +} + +func UpdateSSLConfig(websiteSSL model.WebsiteSSL) error { + websites, _ := websiteRepo.GetBy(websiteRepo.WithWebsiteSSLID(websiteSSL.ID)) + if len(websites) > 0 { + for _, website := range websites { + if err := createPemFile(website, websiteSSL); err != nil { + return buserr.WithMap("ErrUpdateWebsiteSSL", map[string]interface{}{"name": website.PrimaryDomain, "err": err.Error()}, err) + } + } + nginxInstall, err := getAppInstallByKey(constant.AppOpenresty) + if err != nil { + return err + } + if err := opNginx(nginxInstall.ContainerName, constant.NginxReload); err != nil { + return buserr.WithErr(constant.ErrSSLApply, err) + } + } + enable, sslID := GetSystemSSL() + if enable && sslID == websiteSSL.ID { + fileOp := files.NewFileOp() + secretDir := path.Join(global.CONF.System.BaseDir, "1panel/secret") + if err := fileOp.WriteFile(path.Join(secretDir, "server.crt"), strings.NewReader(websiteSSL.Pem), 0600); err != nil { + global.LOG.Errorf("Failed to update the SSL certificate File for 1Panel System domain [%s] , err:%s", websiteSSL.PrimaryDomain, err.Error()) + return err + } + if err := fileOp.WriteFile(path.Join(secretDir, "server.key"), strings.NewReader(websiteSSL.PrivateKey), 0600); err != nil { + global.LOG.Errorf("Failed to update the SSL certificate for 1Panel System domain [%s] , err:%s", websiteSSL.PrimaryDomain, err.Error()) + return err + } + } + return nil +} + +func ChangeHSTSConfig(enable bool, nginxInstall model.AppInstall, website model.Website) error { + includeDir := path.Join(nginxInstall.GetPath(), "www", "sites", website.Alias, "proxy") + fileOp := files.NewFileOp() + if !fileOp.Stat(includeDir) { + return nil + } + err := filepath.Walk(includeDir, func(path string, info os.FileInfo, err error) error { + if err != nil { + return err + } + if !info.IsDir() { + if filepath.Ext(path) == ".conf" { + par, err := parser.NewParser(path) + if err != nil { + return err + } + config, err := par.Parse() + if err != nil { + return err + } + config.FilePath = path + directives := config.Directives + location, ok := directives[0].(*components.Location) + if !ok { + return nil + } + if enable { + location.UpdateDirective("add_header", []string{"Strict-Transport-Security", "\"max-age=31536000\""}) + } else { + location.RemoveDirective("add_header", []string{"Strict-Transport-Security", "\"max-age=31536000\""}) + } + if err = nginx.WriteConfig(config, nginx.IndentedStyle); err != nil { + return buserr.WithErr(constant.ErrUpdateBuWebsite, err) + } + } + } + return nil + }) + if err != nil { + return err + } + return nil +} + +func checkSSLStatus(expireDate time.Time) string { + now := time.Now() + daysUntilExpiry := int(expireDate.Sub(now).Hours() / 24) + + if daysUntilExpiry < 0 { + return "danger" + } else if daysUntilExpiry <= 10 { + return "warning" + } + return "success" +} + +func getResourceContent(fileOp files.FileOp, resourcePath string) (string, error) { + if fileOp.Stat(resourcePath) { + content, err := fileOp.GetContent(resourcePath) + if err != nil { + return "", err + } + return string(content), nil + } + return "", nil +} diff --git a/agent/app/task/task.go b/agent/app/task/task.go new file mode 100644 index 000000000..c1d3ede44 --- /dev/null +++ b/agent/app/task/task.go @@ -0,0 +1,116 @@ +package task + +import ( + "context" + "fmt" + "log" + "os" + "path" + "strconv" + "time" + + "github.com/1Panel-dev/1Panel/agent/constant" + "github.com/1Panel-dev/1Panel/agent/i18n" +) + +type ActionFunc func() error +type RollbackFunc func() + +type Task struct { + Name string + Logger *log.Logger + SubTasks []*SubTask + Rollbacks []RollbackFunc + logFile *os.File +} + +type SubTask struct { + Name string + Retry int + Timeout time.Duration + Action ActionFunc + Rollback RollbackFunc + Error error +} + +func NewTask(name string, taskType string) (*Task, error) { + logPath := path.Join(constant.LogDir, taskType) + //TODO 增加插入到日志表的逻辑 + file, err := os.OpenFile(logPath, os.O_TRUNC|os.O_CREATE|os.O_WRONLY, 0666) + if err != nil { + return nil, fmt.Errorf("failed to open log file: %w", err) + } + logger := log.New(file, "", log.LstdFlags) + return &Task{Name: name, logFile: file, Logger: logger}, nil +} + +func (t *Task) AddSubTask(name string, action ActionFunc, rollback RollbackFunc) { + subTask := &SubTask{Name: name, Retry: 0, Timeout: 10 * time.Minute, Action: action, Rollback: rollback} + t.SubTasks = append(t.SubTasks, subTask) +} + +func (t *Task) AddSubTaskWithOps(name string, action ActionFunc, rollback RollbackFunc, retry int, timeout time.Duration) { + subTask := &SubTask{Name: name, Retry: retry, Timeout: timeout, Action: action, Rollback: rollback} + t.SubTasks = append(t.SubTasks, subTask) +} + +func (s *SubTask) Execute(logger *log.Logger) bool { + logger.Printf(i18n.GetWithName("SubTaskStart", s.Name)) + for i := 0; i < s.Retry+1; i++ { + if i > 0 { + logger.Printf(i18n.GetWithName("TaskRetry", strconv.Itoa(i))) + } + ctx, cancel := context.WithTimeout(context.Background(), s.Timeout) + defer cancel() + + done := make(chan error) + go func() { + done <- s.Action() + }() + + select { + case <-ctx.Done(): + logger.Printf(i18n.GetWithName("TaskTimeout", s.Name)) + case err := <-done: + if err != nil { + s.Error = err + logger.Printf(i18n.GetWithNameAndErr("TaskFailed", s.Name, err)) + } else { + logger.Printf(i18n.GetWithName("TaskSuccess", s.Name)) + return true + } + } + + if i == s.Retry { + if s.Rollback != nil { + s.Rollback() + } + } + time.Sleep(1 * time.Second) + } + if s.Error != nil { + s.Error = fmt.Errorf(i18n.GetWithName("TaskFailed", s.Name)) + } + return false +} + +func (t *Task) Execute() error { + t.Logger.Printf(i18n.GetWithName("TaskStart", t.Name)) + var err error + for _, subTask := range t.SubTasks { + if subTask.Execute(t.Logger) { + if subTask.Rollback != nil { + t.Rollbacks = append(t.Rollbacks, subTask.Rollback) + } + } else { + err = subTask.Error + for _, rollback := range t.Rollbacks { + rollback() + } + break + } + } + t.Logger.Printf(i18n.GetWithName("TaskEnd", t.Name)) + _ = t.logFile.Close() + return err +} diff --git a/agent/buserr/errors.go b/agent/buserr/errors.go new file mode 100644 index 000000000..ad58d1f13 --- /dev/null +++ b/agent/buserr/errors.go @@ -0,0 +1,93 @@ +package buserr + +import ( + "github.com/1Panel-dev/1Panel/agent/i18n" + "github.com/pkg/errors" +) + +type BusinessError struct { + Msg string + Detail interface{} + Map map[string]interface{} + Err error +} + +func (e BusinessError) Error() string { + content := "" + if e.Detail != nil { + content = i18n.GetErrMsg(e.Msg, map[string]interface{}{"detail": e.Detail}) + } else if e.Map != nil { + content = i18n.GetErrMsg(e.Msg, e.Map) + } else { + content = i18n.GetErrMsg(e.Msg, nil) + } + if content == "" { + if e.Err != nil { + return e.Err.Error() + } + return errors.New(e.Msg).Error() + } + return content +} + +func New(Key string) BusinessError { + return BusinessError{ + Msg: Key, + Detail: nil, + Err: nil, + } +} + +func WithDetail(Key string, detail interface{}, err error) BusinessError { + return BusinessError{ + Msg: Key, + Detail: detail, + Err: err, + } +} + +func WithErr(Key string, err error) BusinessError { + paramMap := map[string]interface{}{} + if err != nil { + paramMap["err"] = err + } + return BusinessError{ + Msg: Key, + Map: paramMap, + Err: err, + } +} + +func WithMap(Key string, maps map[string]interface{}, err error) BusinessError { + return BusinessError{ + Msg: Key, + Map: maps, + Err: err, + } +} + +func WithNameAndErr(Key string, name string, err error) BusinessError { + paramMap := map[string]interface{}{} + if name != "" { + paramMap["name"] = name + } + if err != nil { + paramMap["err"] = err.Error() + } + return BusinessError{ + Msg: Key, + Map: paramMap, + Err: err, + } +} + +func WithName(Key string, name string) BusinessError { + paramMap := map[string]interface{}{} + if name != "" { + paramMap["name"] = name + } + return BusinessError{ + Msg: Key, + Map: paramMap, + } +} diff --git a/agent/buserr/multi_err.go b/agent/buserr/multi_err.go new file mode 100644 index 000000000..bf0c875ce --- /dev/null +++ b/agent/buserr/multi_err.go @@ -0,0 +1,23 @@ +package buserr + +import ( + "bytes" + "fmt" + "sort" +) + +type MultiErr map[string]error + +func (e MultiErr) Error() string { + var keys []string + for key := range e { + keys = append(keys, key) + } + sort.Strings(keys) + + buffer := bytes.NewBufferString("") + for _, key := range keys { + buffer.WriteString(fmt.Sprintf("[%s] %s\n", key, e[key])) + } + return buffer.String() +} diff --git a/agent/cmd/server/app/app_config.go b/agent/cmd/server/app/app_config.go new file mode 100644 index 000000000..d4d155998 --- /dev/null +++ b/agent/cmd/server/app/app_config.go @@ -0,0 +1,14 @@ +package app + +import ( + _ "embed" +) + +//go:embed app_config.yml +var Config []byte + +//go:embed logo.png +var Logo []byte + +//go:embed app_param.yml +var Param []byte diff --git a/agent/cmd/server/app/app_config.yml b/agent/cmd/server/app/app_config.yml new file mode 100644 index 000000000..3a2ec0827 --- /dev/null +++ b/agent/cmd/server/app/app_config.yml @@ -0,0 +1,13 @@ +additionalProperties: + key: #应用的 key ,仅限英文,用于在 Linux 创建文件夹 + name: #应用名称 + tags: + - Tool #应用标签,可以有多个,请参照下方的标签列表 + shortDescZh: #应用中文描述,不要超过30个字 + shortDescEn: #应用英文描述 + type: tool #应用类型,区别于应用分类,只能有一个,请参照下方的类型列表 + crossVersionUpdate: #是否可以跨大版本升级 + limit: #应用安装数量限制,0 代表无限制 + website: #官网地址 + github: #github 地址 + document: #文档地址 \ No newline at end of file diff --git a/agent/cmd/server/app/app_param.yml b/agent/cmd/server/app/app_param.yml new file mode 100644 index 000000000..426fa78c6 --- /dev/null +++ b/agent/cmd/server/app/app_param.yml @@ -0,0 +1,10 @@ +additionalProperties: + formFields: + - default: 8080 + edit: true + envKey: PANEL_APP_PORT_HTTP + labelEn: Port + labelZh: 端口 + required: true + rule: paramPort + type: number diff --git a/agent/cmd/server/app/logo.png b/agent/cmd/server/app/logo.png new file mode 100644 index 000000000..6f82a12d5 Binary files /dev/null and b/agent/cmd/server/app/logo.png differ diff --git a/agent/cmd/server/conf/app.yaml b/agent/cmd/server/conf/app.yaml new file mode 100644 index 000000000..0da0e5163 --- /dev/null +++ b/agent/cmd/server/conf/app.yaml @@ -0,0 +1,17 @@ +system: + db_file: 1Panel.db + base_dir: /opt + mode: dev + repo_url: https://resource.fit2cloud.com/1panel/package + app_repo: https://apps-assets.fit2cloud.com + is_demo: false + port: 9999 + username: admin + password: admin123 + +log: + level: debug + time_zone: Asia/Shanghai + log_name: 1Panel + log_suffix: .log + max_backup: 10 diff --git a/agent/cmd/server/conf/conf.go b/agent/cmd/server/conf/conf.go new file mode 100644 index 000000000..6654d8bcf --- /dev/null +++ b/agent/cmd/server/conf/conf.go @@ -0,0 +1,6 @@ +package conf + +import _ "embed" + +//go:embed app.yaml +var AppYaml []byte diff --git a/agent/cmd/server/docs/docs.go b/agent/cmd/server/docs/docs.go new file mode 100644 index 000000000..b004a49f5 --- /dev/null +++ b/agent/cmd/server/docs/docs.go @@ -0,0 +1,23464 @@ +// Code generated by swaggo/swag. DO NOT EDIT. + +package docs + +import "github.com/swaggo/swag" + +const docTemplate = `{ + "schemes": {{ marshal .Schemes }}, + "swagger": "2.0", + "info": { + "description": "{{escape .Description}}", + "title": "{{.Title}}", + "termsOfService": "http://swagger.io/terms/", + "contact": {}, + "license": { + "name": "Apache 2.0", + "url": "http://www.apache.org/licenses/LICENSE-2.0.html" + }, + "version": "{{.Version}}" + }, + "host": "{{.Host}}", + "basePath": "{{.BasePath}}", + "paths": { + "/apps/:key": { + "get": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "通过 key 获取应用信息", + "consumes": [ + "application/json" + ], + "tags": [ + "App" + ], + "summary": "Search app by key", + "parameters": [ + { + "type": "string", + "description": "app key", + "name": "key", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/response.AppDTO" + } + } + } + } + }, + "/apps/checkupdate": { + "get": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "获取应用更新版本", + "tags": [ + "App" + ], + "summary": "Get app list update", + "responses": { + "200": { + "description": "OK" + } + } + } + }, + "/apps/detail/:appId/:version/:type": { + "get": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "通过 appid 获取应用详情", + "consumes": [ + "application/json" + ], + "tags": [ + "App" + ], + "summary": "Search app detail by appid", + "parameters": [ + { + "type": "integer", + "description": "app id", + "name": "appId", + "in": "path", + "required": true + }, + { + "type": "string", + "description": "app 版本", + "name": "version", + "in": "path", + "required": true + }, + { + "type": "string", + "description": "app 类型", + "name": "version", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/response.AppDetailDTO" + } + } + } + } + }, + "/apps/details/:id": { + "get": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "通过 id 获取应用详情", + "consumes": [ + "application/json" + ], + "tags": [ + "App" + ], + "summary": "Get app detail by id", + "parameters": [ + { + "type": "integer", + "description": "id", + "name": "appId", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/response.AppDetailDTO" + } + } + } + } + }, + "/apps/ignored": { + "get": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "获取忽略的应用版本", + "consumes": [ + "application/json" + ], + "tags": [ + "App" + ], + "summary": "Get Ignore App", + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/response.IgnoredApp" + } + } + } + } + }, + "/apps/install": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "安装应用", + "consumes": [ + "application/json" + ], + "tags": [ + "App" + ], + "summary": "Install app", + "parameters": [ + { + "description": "request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/request.AppInstallCreate" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/model.AppInstall" + } + } + }, + "x-panel-log": { + "BeforeFunctions": [ + { + "db": "app_installs", + "input_column": "name", + "input_value": "name", + "isList": false, + "output_column": "app_id", + "output_value": "appId" + }, + { + "db": "apps", + "info": "appId", + "isList": false, + "output_column": "key", + "output_value": "appKey" + } + ], + "bodyKeys": [ + "name" + ], + "formatEN": "Install app [appKey]-[name]", + "formatZH": "安装应用 [appKey]-[name]", + "paramKeys": [] + } + } + }, + "/apps/installed/check": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "检查应用安装情况", + "consumes": [ + "application/json" + ], + "tags": [ + "App" + ], + "summary": "Check app installed", + "parameters": [ + { + "description": "request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/request.AppInstalledInfo" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/response.AppInstalledCheck" + } + } + } + } + }, + "/apps/installed/conf": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "通过 key 获取应用默认配置", + "consumes": [ + "application/json" + ], + "tags": [ + "App" + ], + "summary": "Search default config by key", + "parameters": [ + { + "description": "request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.OperationWithNameAndType" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "string" + } + } + } + } + }, + "/apps/installed/conninfo/:key": { + "get": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "获取应用连接信息", + "consumes": [ + "application/json" + ], + "tags": [ + "App" + ], + "summary": "Search app password by key", + "parameters": [ + { + "description": "request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.OperationWithNameAndType" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "string" + } + } + } + } + }, + "/apps/installed/delete/check/:appInstallId": { + "get": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "删除前检查", + "consumes": [ + "application/json" + ], + "tags": [ + "App" + ], + "summary": "Check before delete", + "parameters": [ + { + "type": "integer", + "description": "App install id", + "name": "appInstallId", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/dto.AppResource" + } + } + } + } + } + }, + "/apps/installed/ignore": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "忽略应用升级版本", + "consumes": [ + "application/json" + ], + "tags": [ + "App" + ], + "summary": "ignore App Update", + "parameters": [ + { + "description": "request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/request.AppInstalledIgnoreUpgrade" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + }, + "x-panel-log": { + "BeforeFunctions": [], + "bodyKeys": [ + "installId" + ], + "formatEN": "Application param update [installId]", + "formatZH": "忽略应用 [installId] 版本升级", + "paramKeys": [] + } + } + }, + "/apps/installed/list": { + "get": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "获取已安装应用列表", + "consumes": [ + "application/json" + ], + "tags": [ + "App" + ], + "summary": "List app installed", + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/dto.AppInstallInfo" + } + } + } + } + } + }, + "/apps/installed/loadport": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "获取应用端口", + "consumes": [ + "application/json" + ], + "tags": [ + "App" + ], + "summary": "Search app port by key", + "parameters": [ + { + "description": "request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.OperationWithNameAndType" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "integer" + } + } + } + } + }, + "/apps/installed/op": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "操作已安装应用", + "consumes": [ + "application/json" + ], + "tags": [ + "App" + ], + "summary": "Operate installed app", + "parameters": [ + { + "description": "request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/request.AppInstalledOperate" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + }, + "x-panel-log": { + "BeforeFunctions": [ + { + "db": "app_installs", + "input_column": "id", + "input_value": "installId", + "isList": false, + "output_column": "app_id", + "output_value": "appId" + }, + { + "db": "app_installs", + "input_column": "id", + "input_value": "installId", + "isList": false, + "output_column": "name", + "output_value": "appName" + }, + { + "db": "apps", + "input_column": "id", + "input_value": "appId", + "isList": false, + "output_column": "key", + "output_value": "appKey" + } + ], + "bodyKeys": [ + "installId", + "operate" + ], + "formatEN": "[operate] App [appKey][appName]", + "formatZH": "[operate] 应用 [appKey][appName]", + "paramKeys": [] + } + } + }, + "/apps/installed/params/:appInstallId": { + "get": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "通过 install id 获取应用参数", + "consumes": [ + "application/json" + ], + "tags": [ + "App" + ], + "summary": "Search params by appInstallId", + "parameters": [ + { + "type": "string", + "description": "request", + "name": "appInstallId", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/response.AppParam" + } + } + } + } + }, + "/apps/installed/params/update": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "修改应用参数", + "consumes": [ + "application/json" + ], + "tags": [ + "App" + ], + "summary": "Change app params", + "parameters": [ + { + "description": "request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/request.AppInstalledUpdate" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + }, + "x-panel-log": { + "BeforeFunctions": [], + "bodyKeys": [ + "installId" + ], + "formatEN": "Application param update [installId]", + "formatZH": "应用参数修改 [installId]", + "paramKeys": [] + } + } + }, + "/apps/installed/port/change": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "修改应用端口", + "consumes": [ + "application/json" + ], + "tags": [ + "App" + ], + "summary": "Change app port", + "parameters": [ + { + "description": "request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/request.PortUpdate" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + }, + "x-panel-log": { + "BeforeFunctions": [], + "bodyKeys": [ + "key", + "name", + "port" + ], + "formatEN": "Application port update [key]-[name] =\u003e [port]", + "formatZH": "应用端口修改 [key]-[name] =\u003e [port]", + "paramKeys": [] + } + } + }, + "/apps/installed/search": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "分页获取已安装应用列表", + "consumes": [ + "application/json" + ], + "tags": [ + "App" + ], + "summary": "Page app installed", + "parameters": [ + { + "description": "request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/request.AppInstalledSearch" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + } + } + }, + "/apps/installed/sync": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "同步已安装应用列表", + "tags": [ + "App" + ], + "summary": "Sync app installed", + "responses": { + "200": { + "description": "OK" + } + }, + "x-panel-log": { + "BeforeFunctions": [], + "bodyKeys": [], + "formatEN": "Sync the list of installed apps", + "formatZH": "同步已安装应用列表", + "paramKeys": [] + } + } + }, + "/apps/installed/update/versions": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "通过 install id 获取应用更新版本", + "consumes": [ + "application/json" + ], + "tags": [ + "App" + ], + "summary": "Search app update version by install id", + "parameters": [ + { + "type": "integer", + "description": "request", + "name": "appInstallId", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/dto.AppVersion" + } + } + } + } + } + }, + "/apps/search": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "获取应用列表", + "consumes": [ + "application/json" + ], + "tags": [ + "App" + ], + "summary": "List apps", + "parameters": [ + { + "description": "request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/request.AppSearch" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + } + } + }, + "/apps/services/:key": { + "get": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "通过 key 获取应用 service", + "consumes": [ + "application/json" + ], + "tags": [ + "App" + ], + "summary": "Search app service by key", + "parameters": [ + { + "type": "string", + "description": "request", + "name": "key", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/response.AppService" + } + } + } + } + } + }, + "/apps/sync": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "同步应用列表", + "tags": [ + "App" + ], + "summary": "Sync app list", + "responses": { + "200": { + "description": "OK" + } + }, + "x-panel-log": { + "BeforeFunctions": [], + "bodyKeys": [], + "formatEN": "App store synchronization", + "formatZH": "应用商店同步", + "paramKeys": [] + } + } + }, + "/auth/captcha": { + "get": { + "description": "加载验证码", + "tags": [ + "Auth" + ], + "summary": "Load captcha", + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/dto.CaptchaResponse" + } + } + } + } + }, + "/auth/demo": { + "get": { + "description": "判断是否为demo环境", + "tags": [ + "Auth" + ], + "summary": "Check System isDemo", + "responses": { + "200": { + "description": "OK" + } + } + } + }, + "/auth/issafety": { + "get": { + "description": "获取系统安全登录状态", + "tags": [ + "Auth" + ], + "summary": "Load safety status", + "responses": { + "200": { + "description": "OK" + } + } + } + }, + "/auth/language": { + "get": { + "description": "获取系统语言设置", + "tags": [ + "Auth" + ], + "summary": "Load System Language", + "responses": { + "200": { + "description": "OK" + } + } + } + }, + "/auth/login": { + "post": { + "description": "用户登录", + "consumes": [ + "application/json" + ], + "tags": [ + "Auth" + ], + "summary": "User login", + "parameters": [ + { + "type": "string", + "description": "安全入口 base64 加密串", + "name": "EntranceCode", + "in": "header", + "required": true + }, + { + "description": "request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.Login" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/dto.UserLoginInfo" + } + } + } + } + }, + "/auth/logout": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "用户登出", + "tags": [ + "Auth" + ], + "summary": "User logout", + "responses": { + "200": { + "description": "OK" + } + } + } + }, + "/auth/mfalogin": { + "post": { + "description": "用户 mfa 登录", + "consumes": [ + "application/json" + ], + "tags": [ + "Auth" + ], + "summary": "User login with mfa", + "parameters": [ + { + "description": "request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.MFALogin" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/dto.UserLoginInfo" + }, + "headers": { + "EntranceCode": { + "type": "string", + "description": "安全入口" + } + } + } + } + } + }, + "/containers": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "创建容器", + "consumes": [ + "application/json" + ], + "tags": [ + "Container" + ], + "summary": "Create container", + "parameters": [ + { + "description": "request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.ContainerOperate" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + }, + "x-panel-log": { + "BeforeFunctions": [], + "bodyKeys": [ + "name", + "image" + ], + "formatEN": "create container [name][image]", + "formatZH": "创建容器 [name][image]", + "paramKeys": [] + } + } + }, + "/containers/clean/log": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "清理容器日志", + "consumes": [ + "application/json" + ], + "tags": [ + "Container" + ], + "summary": "Clean container log", + "parameters": [ + { + "description": "request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.OperationWithName" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + }, + "x-panel-log": { + "BeforeFunctions": [], + "bodyKeys": [ + "name" + ], + "formatEN": "clean container [name] logs", + "formatZH": "清理容器 [name] 日志", + "paramKeys": [] + } + } + }, + "/containers/commit": { + "post": { + "description": "容器提交生成新镜像", + "consumes": [ + "application/json" + ], + "tags": [ + "Container" + ], + "summary": "Commit Container", + "parameters": [ + { + "description": "request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.ContainerCommit" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + } + } + }, + "/containers/compose": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "创建容器编排", + "consumes": [ + "application/json" + ], + "tags": [ + "Container Compose" + ], + "summary": "Create compose", + "parameters": [ + { + "description": "request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.ComposeCreate" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + }, + "x-panel-log": { + "BeforeFunctions": [], + "bodyKeys": [ + "name" + ], + "formatEN": "create compose [name]", + "formatZH": "创建 compose [name]", + "paramKeys": [] + } + } + }, + "/containers/compose/operate": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "容器编排操作", + "consumes": [ + "application/json" + ], + "tags": [ + "Container Compose" + ], + "summary": "Operate compose", + "parameters": [ + { + "description": "request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.ComposeOperation" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + }, + "x-panel-log": { + "BeforeFunctions": [], + "bodyKeys": [ + "name", + "operation" + ], + "formatEN": "compose [operation] [name]", + "formatZH": "compose [operation] [name]", + "paramKeys": [] + } + } + }, + "/containers/compose/search": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "获取编排列表分页", + "consumes": [ + "application/json" + ], + "tags": [ + "Container Compose" + ], + "summary": "Page composes", + "parameters": [ + { + "description": "request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.SearchWithPage" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/dto.PageResult" + } + } + } + } + }, + "/containers/compose/search/log": { + "get": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "docker-compose 日志", + "tags": [ + "Container Compose" + ], + "summary": "Container Compose logs", + "parameters": [ + { + "type": "string", + "description": "compose 文件地址", + "name": "compose", + "in": "query" + }, + { + "type": "string", + "description": "时间筛选", + "name": "since", + "in": "query" + }, + { + "type": "string", + "description": "是否追踪", + "name": "follow", + "in": "query" + }, + { + "type": "string", + "description": "显示行号", + "name": "tail", + "in": "query" + } + ], + "responses": {} + } + }, + "/containers/compose/test": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "测试 compose 是否可用", + "consumes": [ + "application/json" + ], + "tags": [ + "Container Compose" + ], + "summary": "Test compose", + "parameters": [ + { + "description": "request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.ComposeCreate" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + }, + "x-panel-log": { + "BeforeFunctions": [], + "bodyKeys": [ + "name" + ], + "formatEN": "check compose [name]", + "formatZH": "检测 compose [name] 格式", + "paramKeys": [] + } + } + }, + "/containers/compose/update": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "更新容器编排", + "consumes": [ + "application/json" + ], + "tags": [ + "Container Compose" + ], + "summary": "Update compose", + "parameters": [ + { + "description": "request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.ComposeUpdate" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + }, + "x-panel-log": { + "BeforeFunctions": [], + "bodyKeys": [ + "name" + ], + "formatEN": "update compose information [name]", + "formatZH": "更新 compose [name]", + "paramKeys": [] + } + } + }, + "/containers/daemonjson": { + "get": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "获取 docker 配置信息", + "produces": [ + "application/json" + ], + "tags": [ + "Container Docker" + ], + "summary": "Load docker daemon.json", + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/dto.DaemonJsonConf" + } + } + } + } + }, + "/containers/daemonjson/file": { + "get": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "获取 docker 配置信息(表单)", + "produces": [ + "application/json" + ], + "tags": [ + "Container Docker" + ], + "summary": "Load docker daemon.json", + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "string" + } + } + } + } + }, + "/containers/daemonjson/update": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "修改 docker 配置信息", + "consumes": [ + "application/json" + ], + "tags": [ + "Container Docker" + ], + "summary": "Update docker daemon.json", + "parameters": [ + { + "description": "request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.SettingUpdate" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + }, + "x-panel-log": { + "BeforeFunctions": [], + "bodyKeys": [ + "key", + "value" + ], + "formatEN": "Updated configuration [key]", + "formatZH": "更新配置 [key]", + "paramKeys": [] + } + } + }, + "/containers/daemonjson/update/byfile": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "上传替换 docker 配置文件", + "consumes": [ + "application/json" + ], + "tags": [ + "Container Docker" + ], + "summary": "Update docker daemon.json by upload file", + "parameters": [ + { + "description": "request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.DaemonJsonUpdateByFile" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + }, + "x-panel-log": { + "BeforeFunctions": [], + "bodyKeys": [], + "formatEN": "Updated configuration file", + "formatZH": "更新配置文件", + "paramKeys": [] + } + } + }, + "/containers/docker/operate": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "Docker 操作", + "consumes": [ + "application/json" + ], + "tags": [ + "Container Docker" + ], + "summary": "Operate docker", + "parameters": [ + { + "description": "request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.DockerOperation" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + }, + "x-panel-log": { + "BeforeFunctions": [], + "bodyKeys": [ + "operation" + ], + "formatEN": "[operation] docker service", + "formatZH": "docker 服务 [operation]", + "paramKeys": [] + } + } + }, + "/containers/docker/status": { + "get": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "获取 docker 服务状态", + "produces": [ + "application/json" + ], + "tags": [ + "Container Docker" + ], + "summary": "Load docker status", + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "string" + } + } + } + } + }, + "/containers/download/log": { + "post": { + "description": "下载容器日志", + "responses": {} + } + }, + "/containers/image": { + "get": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "获取镜像名称列表", + "produces": [ + "application/json" + ], + "tags": [ + "Container Image" + ], + "summary": "load images options", + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/dto.Options" + } + } + } + } + } + }, + "/containers/image/all": { + "get": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "获取所有镜像列表", + "produces": [ + "application/json" + ], + "tags": [ + "Container Image" + ], + "summary": "List all images", + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/dto.ImageInfo" + } + } + } + } + } + }, + "/containers/image/build": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "构建镜像", + "consumes": [ + "application/json" + ], + "tags": [ + "Container Image" + ], + "summary": "Build image", + "parameters": [ + { + "description": "request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.ImageBuild" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "string" + } + } + }, + "x-panel-log": { + "BeforeFunctions": [], + "bodyKeys": [ + "name" + ], + "formatEN": "build image [name]", + "formatZH": "构建镜像 [name]", + "paramKeys": [] + } + } + }, + "/containers/image/load": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "导入镜像", + "consumes": [ + "application/json" + ], + "tags": [ + "Container Image" + ], + "summary": "Load image", + "parameters": [ + { + "description": "request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.ImageLoad" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + }, + "x-panel-log": { + "BeforeFunctions": [], + "bodyKeys": [ + "path" + ], + "formatEN": "load image from [path]", + "formatZH": "从 [path] 加载镜像", + "paramKeys": [] + } + } + }, + "/containers/image/pull": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "拉取镜像", + "consumes": [ + "application/json" + ], + "tags": [ + "Container Image" + ], + "summary": "Pull image", + "parameters": [ + { + "description": "request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.ImagePull" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "string" + } + } + }, + "x-panel-log": { + "BeforeFunctions": [ + { + "db": "image_repos", + "input_column": "id", + "input_value": "repoID", + "isList": false, + "output_column": "name", + "output_value": "reponame" + } + ], + "bodyKeys": [ + "repoID", + "imageName" + ], + "formatEN": "image pull [reponame][imageName]", + "formatZH": "镜像拉取 [reponame][imageName]", + "paramKeys": [] + } + } + }, + "/containers/image/push": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "推送镜像", + "consumes": [ + "application/json" + ], + "tags": [ + "Container Image" + ], + "summary": "Push image", + "parameters": [ + { + "description": "request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.ImagePush" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "string" + } + } + }, + "x-panel-log": { + "BeforeFunctions": [ + { + "db": "image_repos", + "input_column": "id", + "input_value": "repoID", + "isList": false, + "output_column": "name", + "output_value": "reponame" + } + ], + "bodyKeys": [ + "repoID", + "tagName", + "name" + ], + "formatEN": "push [tagName] to [reponame][name]", + "formatZH": "[tagName] 推送到 [reponame][name]", + "paramKeys": [] + } + } + }, + "/containers/image/remove": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "删除镜像", + "consumes": [ + "application/json" + ], + "tags": [ + "Container Image" + ], + "summary": "Delete image", + "parameters": [ + { + "description": "request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.BatchDelete" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + }, + "x-panel-log": { + "BeforeFunctions": [], + "bodyKeys": [ + "names" + ], + "formatEN": "remove image [names]", + "formatZH": "移除镜像 [names]", + "paramKeys": [] + } + } + }, + "/containers/image/save": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "导出镜像", + "consumes": [ + "application/json" + ], + "tags": [ + "Container Image" + ], + "summary": "Save image", + "parameters": [ + { + "description": "request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.ImageSave" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + }, + "x-panel-log": { + "BeforeFunctions": [], + "bodyKeys": [ + "tagName", + "path", + "name" + ], + "formatEN": "save [tagName] as [path]/[name]", + "formatZH": "保留 [tagName] 为 [path]/[name]", + "paramKeys": [] + } + } + }, + "/containers/image/search": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "获取镜像列表分页", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Container Image" + ], + "summary": "Page images", + "parameters": [ + { + "description": "request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.SearchWithPage" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/dto.PageResult" + } + } + } + } + }, + "/containers/image/tag": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "Tag 镜像", + "consumes": [ + "application/json" + ], + "tags": [ + "Container Image" + ], + "summary": "Tag image", + "parameters": [ + { + "description": "request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.ImageTag" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + }, + "x-panel-log": { + "BeforeFunctions": [ + { + "db": "image_repos", + "input_column": "id", + "input_value": "repoID", + "isList": false, + "output_column": "name", + "output_value": "reponame" + } + ], + "bodyKeys": [ + "repoID", + "targetName" + ], + "formatEN": "tag image [reponame][targetName]", + "formatZH": "tag 镜像 [reponame][targetName]", + "paramKeys": [] + } + } + }, + "/containers/info": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "获取容器表单信息", + "consumes": [ + "application/json" + ], + "tags": [ + "Container" + ], + "summary": "Load container info", + "parameters": [ + { + "description": "request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.OperationWithName" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/dto.ContainerOperate" + } + } + } + } + }, + "/containers/inspect": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "容器详情", + "consumes": [ + "application/json" + ], + "tags": [ + "Container" + ], + "summary": "Container inspect", + "parameters": [ + { + "description": "request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.InspectReq" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "string" + } + } + } + } + }, + "/containers/ipv6option/update": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "修改 docker ipv6 配置", + "consumes": [ + "application/json" + ], + "tags": [ + "Container Docker" + ], + "summary": "Update docker daemon.json ipv6 option", + "parameters": [ + { + "description": "request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.LogOption" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + }, + "x-panel-log": { + "BeforeFunctions": [], + "bodyKeys": [], + "formatEN": "Updated the ipv6 option", + "formatZH": "更新 ipv6 配置", + "paramKeys": [] + } + } + }, + "/containers/limit": { + "get": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "获取容器限制", + "summary": "Load container limits", + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/dto.ResourceLimit" + } + } + } + } + }, + "/containers/list": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "获取容器名称", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Container" + ], + "summary": "List containers", + "responses": { + "200": { + "description": "OK" + } + } + } + }, + "/containers/list/stats": { + "get": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "获取容器列表资源占用", + "summary": "Load container stats", + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/dto.ContainerListStats" + } + } + } + } + } + }, + "/containers/load/log": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "获取容器操作日志", + "consumes": [ + "application/json" + ], + "tags": [ + "Container" + ], + "summary": "Load container log", + "parameters": [ + { + "description": "request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.OperationWithNameAndType" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + } + } + }, + "/containers/logoption/update": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "修改 docker 日志配置", + "consumes": [ + "application/json" + ], + "tags": [ + "Container Docker" + ], + "summary": "Update docker daemon.json log option", + "parameters": [ + { + "description": "request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.LogOption" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + }, + "x-panel-log": { + "BeforeFunctions": [], + "bodyKeys": [], + "formatEN": "Updated the log option", + "formatZH": "更新日志配置", + "paramKeys": [] + } + } + }, + "/containers/network": { + "get": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "获取容器网络列表", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Container Network" + ], + "summary": "List networks", + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/dto.Options" + } + } + } + } + }, + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "创建容器网络", + "consumes": [ + "application/json" + ], + "tags": [ + "Container Network" + ], + "summary": "Create network", + "parameters": [ + { + "description": "request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.NetworkCreate" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + }, + "x-panel-log": { + "BeforeFunctions": [], + "bodyKeys": [ + "name" + ], + "formatEN": "create container network [name]", + "formatZH": "创建容器网络 name", + "paramKeys": [] + } + } + }, + "/containers/network/del": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "删除容器网络", + "consumes": [ + "application/json" + ], + "tags": [ + "Container Network" + ], + "summary": "Delete network", + "parameters": [ + { + "description": "request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.BatchDelete" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + }, + "x-panel-log": { + "BeforeFunctions": [], + "bodyKeys": [ + "names" + ], + "formatEN": "delete container network [names]", + "formatZH": "删除容器网络 [names]", + "paramKeys": [] + } + } + }, + "/containers/network/search": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "获取容器网络列表分页", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Container Network" + ], + "summary": "Page networks", + "parameters": [ + { + "description": "request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.SearchWithPage" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/dto.PageResult" + } + } + } + } + }, + "/containers/operate": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "容器操作", + "consumes": [ + "application/json" + ], + "tags": [ + "Container" + ], + "summary": "Operate Container", + "parameters": [ + { + "description": "request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.ContainerOperation" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + }, + "x-panel-log": { + "BeforeFunctions": [], + "bodyKeys": [ + "names", + "operation" + ], + "formatEN": "container [operation] [names]", + "formatZH": "容器 [names] 执行 [operation]", + "paramKeys": [] + } + } + }, + "/containers/prune": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "容器清理", + "consumes": [ + "application/json" + ], + "tags": [ + "Container" + ], + "summary": "Clean container", + "parameters": [ + { + "description": "request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.ContainerPrune" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/dto.ContainerPruneReport" + } + } + }, + "x-panel-log": { + "BeforeFunctions": [], + "bodyKeys": [ + "pruneType" + ], + "formatEN": "clean container [pruneType]", + "formatZH": "清理容器 [pruneType]", + "paramKeys": [] + } + } + }, + "/containers/rename": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "容器重命名", + "consumes": [ + "application/json" + ], + "tags": [ + "Container" + ], + "summary": "Rename Container", + "parameters": [ + { + "description": "request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.ContainerRename" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + }, + "x-panel-log": { + "BeforeFunctions": [], + "bodyKeys": [ + "name", + "newName" + ], + "formatEN": "rename container [name] =\u003e [newName]", + "formatZH": "容器重命名 [name] =\u003e [newName]", + "paramKeys": [] + } + } + }, + "/containers/repo": { + "get": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "获取镜像仓库列表", + "produces": [ + "application/json" + ], + "tags": [ + "Container Image-repo" + ], + "summary": "List image repos", + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/dto.ImageRepoOption" + } + } + } + } + }, + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "创建镜像仓库", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Container Image-repo" + ], + "summary": "Create image repo", + "parameters": [ + { + "description": "request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.ImageRepoDelete" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + }, + "x-panel-log": { + "BeforeFunctions": [], + "bodyKeys": [ + "name" + ], + "formatEN": "create image repo [name]", + "formatZH": "创建镜像仓库 [name]", + "paramKeys": [] + } + } + }, + "/containers/repo/del": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "删除镜像仓库", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Container Image-repo" + ], + "summary": "Delete image repo", + "parameters": [ + { + "description": "request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.ImageRepoDelete" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + }, + "x-panel-log": { + "BeforeFunctions": [ + { + "db": "image_repos", + "input_column": "id", + "input_value": "ids", + "isList": true, + "output_column": "name", + "output_value": "names" + } + ], + "bodyKeys": [ + "ids" + ], + "formatEN": "delete image repo [names]", + "formatZH": "删除镜像仓库 [names]", + "paramKeys": [] + } + } + }, + "/containers/repo/search": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "获取镜像仓库列表分页", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Container Image-repo" + ], + "summary": "Page image repos", + "parameters": [ + { + "description": "request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.SearchWithPage" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/dto.PageResult" + } + } + } + } + }, + "/containers/repo/status": { + "get": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "获取 docker 仓库状态", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Container Image-repo" + ], + "summary": "Load repo status", + "parameters": [ + { + "description": "request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.OperateByID" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + } + } + }, + "/containers/repo/update": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "更新镜像仓库", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Container Image-repo" + ], + "summary": "Update image repo", + "parameters": [ + { + "description": "request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.ImageRepoUpdate" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + }, + "x-panel-log": { + "BeforeFunctions": [ + { + "db": "image_repos", + "input_column": "id", + "input_value": "id", + "isList": false, + "output_column": "name", + "output_value": "name" + } + ], + "bodyKeys": [ + "id" + ], + "formatEN": "update image repo information [name]", + "formatZH": "更新镜像仓库 [name]", + "paramKeys": [] + } + } + }, + "/containers/search": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "获取容器列表分页", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Container" + ], + "summary": "Page containers", + "parameters": [ + { + "description": "request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.PageContainer" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/dto.PageResult" + } + } + } + } + }, + "/containers/search/log": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "容器日志", + "tags": [ + "Container" + ], + "summary": "Container logs", + "parameters": [ + { + "type": "string", + "description": "容器名称", + "name": "container", + "in": "query" + }, + { + "type": "string", + "description": "时间筛选", + "name": "since", + "in": "query" + }, + { + "type": "string", + "description": "是否追踪", + "name": "follow", + "in": "query" + }, + { + "type": "string", + "description": "显示行号", + "name": "tail", + "in": "query" + } + ], + "responses": {} + } + }, + "/containers/stats/:id": { + "get": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "容器监控信息", + "tags": [ + "Container" + ], + "summary": "Container stats", + "parameters": [ + { + "type": "integer", + "description": "容器id", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/dto.ContainerStats" + } + } + } + } + }, + "/containers/template": { + "get": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "获取容器编排模版列表", + "produces": [ + "application/json" + ], + "tags": [ + "Container Compose-template" + ], + "summary": "List compose templates", + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/dto.ComposeTemplateInfo" + } + } + } + } + }, + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "创建容器编排模版", + "consumes": [ + "application/json" + ], + "tags": [ + "Container Compose-template" + ], + "summary": "Create compose template", + "parameters": [ + { + "description": "request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.ComposeTemplateCreate" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + }, + "x-panel-log": { + "BeforeFunctions": [], + "bodyKeys": [ + "name" + ], + "formatEN": "create compose template [name]", + "formatZH": "创建 compose 模版 [name]", + "paramKeys": [] + } + } + }, + "/containers/template/del": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "删除容器编排模版", + "consumes": [ + "application/json" + ], + "tags": [ + "Container Compose-template" + ], + "summary": "Delete compose template", + "parameters": [ + { + "description": "request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.BatchDelete" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + }, + "x-panel-log": { + "BeforeFunctions": [ + { + "db": "compose_templates", + "input_column": "id", + "input_value": "ids", + "isList": true, + "output_column": "name", + "output_value": "names" + } + ], + "bodyKeys": [ + "ids" + ], + "formatEN": "delete compose template [names]", + "formatZH": "删除 compose 模版 [names]", + "paramKeys": [] + } + } + }, + "/containers/template/search": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "获取容器编排模版列表分页", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Container Compose-template" + ], + "summary": "Page compose templates", + "parameters": [ + { + "description": "request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.SearchWithPage" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/dto.PageResult" + } + } + } + } + }, + "/containers/template/update": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "更新容器编排模版", + "consumes": [ + "application/json" + ], + "tags": [ + "Container Compose-template" + ], + "summary": "Update compose template", + "parameters": [ + { + "description": "request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.ComposeTemplateUpdate" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + }, + "x-panel-log": { + "BeforeFunctions": [ + { + "db": "compose_templates", + "input_column": "id", + "input_value": "id", + "isList": false, + "output_column": "name", + "output_value": "name" + } + ], + "bodyKeys": [ + "id" + ], + "formatEN": "update compose template information [name]", + "formatZH": "更新 compose 模版 [name]", + "paramKeys": [] + } + } + }, + "/containers/update": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "更新容器", + "consumes": [ + "application/json" + ], + "tags": [ + "Container" + ], + "summary": "Update container", + "parameters": [ + { + "description": "request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.ContainerOperate" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + }, + "x-panel-log": { + "BeforeFunctions": [], + "bodyKeys": [ + "name", + "image" + ], + "formatEN": "update container [name][image]", + "formatZH": "更新容器 [name][image]", + "paramKeys": [] + } + } + }, + "/containers/upgrade": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "更新容器镜像", + "consumes": [ + "application/json" + ], + "tags": [ + "Container" + ], + "summary": "Upgrade container", + "parameters": [ + { + "description": "request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.ContainerUpgrade" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + }, + "x-panel-log": { + "BeforeFunctions": [], + "bodyKeys": [ + "name", + "image" + ], + "formatEN": "upgrade container image [name][image]", + "formatZH": "更新容器镜像 [name][image]", + "paramKeys": [] + } + } + }, + "/containers/volume": { + "get": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "获取容器存储卷列表", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Container Volume" + ], + "summary": "List volumes", + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/dto.Options" + } + } + } + } + }, + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "创建容器存储卷", + "consumes": [ + "application/json" + ], + "tags": [ + "Container Volume" + ], + "summary": "Create volume", + "parameters": [ + { + "description": "request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.VolumeCreate" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + }, + "x-panel-log": { + "BeforeFunctions": [], + "bodyKeys": [ + "name" + ], + "formatEN": "create container volume [name]", + "formatZH": "创建容器存储卷 [name]", + "paramKeys": [] + } + } + }, + "/containers/volume/del": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "删除容器存储卷", + "consumes": [ + "application/json" + ], + "tags": [ + "Container Volume" + ], + "summary": "Delete volume", + "parameters": [ + { + "description": "request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.BatchDelete" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + }, + "x-panel-log": { + "BeforeFunctions": [], + "bodyKeys": [ + "names" + ], + "formatEN": "delete container volume [names]", + "formatZH": "删除容器存储卷 [names]", + "paramKeys": [] + } + } + }, + "/containers/volume/search": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "获取容器存储卷分页", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Container Volume" + ], + "summary": "Page volumes", + "parameters": [ + { + "description": "request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.SearchWithPage" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/dto.PageResult" + } + } + } + } + }, + "/cronjobs": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "创建计划任务", + "consumes": [ + "application/json" + ], + "tags": [ + "Cronjob" + ], + "summary": "Create cronjob", + "parameters": [ + { + "description": "request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.CronjobCreate" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + }, + "x-panel-log": { + "BeforeFunctions": [], + "bodyKeys": [ + "type", + "name" + ], + "formatEN": "create cronjob [type][name]", + "formatZH": "创建计划任务 [type][name]", + "paramKeys": [] + } + } + }, + "/cronjobs/del": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "删除计划任务", + "consumes": [ + "application/json" + ], + "tags": [ + "Cronjob" + ], + "summary": "Delete cronjob", + "parameters": [ + { + "description": "request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.CronjobBatchDelete" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + }, + "x-panel-log": { + "BeforeFunctions": [ + { + "db": "cronjobs", + "input_column": "id", + "input_value": "ids", + "isList": true, + "output_column": "name", + "output_value": "names" + } + ], + "bodyKeys": [ + "ids" + ], + "formatEN": "delete cronjob [names]", + "formatZH": "删除计划任务 [names]", + "paramKeys": [] + } + } + }, + "/cronjobs/download": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "下载计划任务记录", + "consumes": [ + "application/json" + ], + "tags": [ + "Cronjob" + ], + "summary": "Download cronjob records", + "parameters": [ + { + "description": "request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.CronjobDownload" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + }, + "x-panel-log": { + "BeforeFunctions": [ + { + "db": "job_records", + "input_column": "id", + "input_value": "recordID", + "isList": false, + "output_column": "file", + "output_value": "file" + } + ], + "bodyKeys": [ + "recordID" + ], + "formatEN": "download the cronjob record [file]", + "formatZH": "下载计划任务记录 [file]", + "paramKeys": [] + } + } + }, + "/cronjobs/handle": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "手动执行计划任务", + "consumes": [ + "application/json" + ], + "tags": [ + "Cronjob" + ], + "summary": "Handle cronjob once", + "parameters": [ + { + "description": "request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.OperateByID" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + }, + "x-panel-log": { + "BeforeFunctions": [ + { + "db": "cronjobs", + "input_column": "id", + "input_value": "id", + "isList": false, + "output_column": "name", + "output_value": "name" + } + ], + "bodyKeys": [ + "id" + ], + "formatEN": "manually execute the cronjob [name]", + "formatZH": "手动执行计划任务 [name]", + "paramKeys": [] + } + } + }, + "/cronjobs/records/clean": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "清空计划任务记录", + "consumes": [ + "application/json" + ], + "tags": [ + "Cronjob" + ], + "summary": "Clean job records", + "parameters": [ + { + "description": "request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.CronjobClean" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + }, + "x-panel-log": { + "BeforeFunctions": [ + { + "db": "cronjobs", + "input_column": "id", + "input_value": "id", + "isList": false, + "output_column": "name", + "output_value": "name" + } + ], + "bodyKeys": [ + "id" + ], + "formatEN": "clean cronjob [name] records", + "formatZH": "清空计划任务记录 [name]", + "paramKeys": [] + } + } + }, + "/cronjobs/records/log": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "获取计划任务记录日志", + "consumes": [ + "application/json" + ], + "tags": [ + "Cronjob" + ], + "summary": "Load Cronjob record log", + "parameters": [ + { + "description": "request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.OperateByID" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + } + } + }, + "/cronjobs/search": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "获取计划任务分页", + "consumes": [ + "application/json" + ], + "tags": [ + "Cronjob" + ], + "summary": "Page cronjobs", + "parameters": [ + { + "description": "request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.PageCronjob" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/dto.PageResult" + } + } + } + } + }, + "/cronjobs/search/records": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "获取计划任务记录", + "consumes": [ + "application/json" + ], + "tags": [ + "Cronjob" + ], + "summary": "Page job records", + "parameters": [ + { + "description": "request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.SearchRecord" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/dto.PageResult" + } + } + } + } + }, + "/cronjobs/status": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "更新计划任务状态", + "consumes": [ + "application/json" + ], + "tags": [ + "Cronjob" + ], + "summary": "Update cronjob status", + "parameters": [ + { + "description": "request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.CronjobUpdateStatus" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + }, + "x-panel-log": { + "BeforeFunctions": [ + { + "db": "cronjobs", + "input_column": "id", + "input_value": "id", + "isList": false, + "output_column": "name", + "output_value": "name" + } + ], + "bodyKeys": [ + "id", + "status" + ], + "formatEN": "change the status of cronjob [name] to [status].", + "formatZH": "修改计划任务 [name] 状态为 [status]", + "paramKeys": [] + } + } + }, + "/cronjobs/update": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "更新计划任务", + "consumes": [ + "application/json" + ], + "tags": [ + "Cronjob" + ], + "summary": "Update cronjob", + "parameters": [ + { + "description": "request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.CronjobUpdate" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + }, + "x-panel-log": { + "BeforeFunctions": [ + { + "db": "cronjobs", + "input_column": "id", + "input_value": "id", + "isList": false, + "output_column": "name", + "output_value": "name" + } + ], + "bodyKeys": [ + "id" + ], + "formatEN": "update cronjob [name]", + "formatZH": "更新计划任务 [name]", + "paramKeys": [] + } + } + }, + "/dashboard/base/:ioOption/:netOption": { + "get": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "获取首页基础数据", + "consumes": [ + "application/json" + ], + "tags": [ + "Dashboard" + ], + "summary": "Load dashboard base info", + "parameters": [ + { + "type": "string", + "description": "request", + "name": "ioOption", + "in": "path", + "required": true + }, + { + "type": "string", + "description": "request", + "name": "netOption", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/dto.DashboardBase" + } + } + } + } + }, + "/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": [ + { + "ApiKeyAuth": [] + } + ], + "description": "获取首页实时数据", + "consumes": [ + "application/json" + ], + "tags": [ + "Dashboard" + ], + "summary": "Load dashboard current info", + "parameters": [ + { + "type": "string", + "description": "request", + "name": "ioOption", + "in": "path", + "required": true + }, + { + "type": "string", + "description": "request", + "name": "netOption", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/dto.DashboardCurrent" + } + } + } + } + }, + "/dashboard/system/restart/:operation": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "重启服务器/面板", + "consumes": [ + "application/json" + ], + "tags": [ + "Dashboard" + ], + "summary": "System restart", + "parameters": [ + { + "type": "string", + "description": "request", + "name": "operation", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK" + } + } + } + }, + "/databases": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "创建 mysql 数据库", + "consumes": [ + "application/json" + ], + "tags": [ + "Database Mysql" + ], + "summary": "Create mysql database", + "parameters": [ + { + "description": "request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.MysqlDBCreate" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + }, + "x-panel-log": { + "BeforeFunctions": [], + "bodyKeys": [ + "name" + ], + "formatEN": "create mysql database [name]", + "formatZH": "创建 mysql 数据库 [name]", + "paramKeys": [] + } + } + }, + "/databases/bind": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "绑定 mysql 数据库用户", + "consumes": [ + "application/json" + ], + "tags": [ + "Database Mysql" + ], + "summary": "Bind user of mysql database", + "parameters": [ + { + "description": "request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.BindUser" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + }, + "x-panel-log": { + "BeforeFunctions": [], + "bodyKeys": [ + "database", + "username" + ], + "formatEN": "bind mysql database [database] [username]", + "formatZH": "绑定 mysql 数据库名 [database] [username]", + "paramKeys": [] + } + } + }, + "/databases/change/access": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "修改 mysql 访问权限", + "consumes": [ + "application/json" + ], + "tags": [ + "Database Mysql" + ], + "summary": "Change mysql access", + "parameters": [ + { + "description": "request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.ChangeDBInfo" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + }, + "x-panel-log": { + "BeforeFunctions": [ + { + "db": "database_mysqls", + "input_column": "id", + "input_value": "id", + "isList": false, + "output_column": "name", + "output_value": "name" + } + ], + "bodyKeys": [ + "id" + ], + "formatEN": "Update database [name] access", + "formatZH": "更新数据库 [name] 访问权限", + "paramKeys": [] + } + } + }, + "/databases/change/password": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "修改 mysql 密码", + "consumes": [ + "application/json" + ], + "tags": [ + "Database Mysql" + ], + "summary": "Change mysql password", + "parameters": [ + { + "description": "request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.ChangeDBInfo" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + }, + "x-panel-log": { + "BeforeFunctions": [ + { + "db": "database_mysqls", + "input_column": "id", + "input_value": "id", + "isList": false, + "output_column": "name", + "output_value": "name" + } + ], + "bodyKeys": [ + "id" + ], + "formatEN": "Update database [name] password", + "formatZH": "更新数据库 [name] 密码", + "paramKeys": [] + } + } + }, + "/databases/common/info": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "获取数据库基础信息", + "consumes": [ + "application/json" + ], + "tags": [ + "Database Common" + ], + "summary": "Load base info", + "parameters": [ + { + "description": "request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.OperationWithNameAndType" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/dto.DBBaseInfo" + } + } + } + } + }, + "/databases/common/load/file": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "获取数据库配置文件", + "consumes": [ + "application/json" + ], + "tags": [ + "Database Common" + ], + "summary": "Load Database conf", + "parameters": [ + { + "description": "request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.OperationWithNameAndType" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + } + } + }, + "/databases/common/update/conf": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "上传替换配置文件", + "consumes": [ + "application/json" + ], + "tags": [ + "Database Common" + ], + "summary": "Update conf by upload file", + "parameters": [ + { + "description": "request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.DBConfUpdateByFile" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + }, + "x-panel-log": { + "BeforeFunctions": [], + "bodyKeys": [ + "type", + "database" + ], + "formatEN": "update the [type] [database] database configuration information", + "formatZH": "更新 [type] 数据库 [database] 配置信息", + "paramKeys": [] + } + } + }, + "/databases/db": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "创建远程数据库", + "consumes": [ + "application/json" + ], + "tags": [ + "Database" + ], + "summary": "Create database", + "parameters": [ + { + "description": "request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.DatabaseCreate" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + }, + "x-panel-log": { + "BeforeFunctions": [], + "bodyKeys": [ + "name", + "type" + ], + "formatEN": "create database [name][type]", + "formatZH": "创建远程数据库 [name][type]", + "paramKeys": [] + } + } + }, + "/databases/db/:name": { + "get": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "获取远程数据库", + "tags": [ + "Database" + ], + "summary": "Get databases", + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/dto.DatabaseInfo" + } + } + } + } + }, + "/databases/db/check": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "检测远程数据库连接性", + "consumes": [ + "application/json" + ], + "tags": [ + "Database" + ], + "summary": "Check database", + "parameters": [ + { + "description": "request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.DatabaseCreate" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + }, + "x-panel-log": { + "BeforeFunctions": [], + "bodyKeys": [ + "name", + "type" + ], + "formatEN": "check if database [name][type] is connectable", + "formatZH": "检测远程数据库 [name][type] 连接性", + "paramKeys": [] + } + } + }, + "/databases/db/del": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "删除远程数据库", + "consumes": [ + "application/json" + ], + "tags": [ + "Database" + ], + "summary": "Delete database", + "parameters": [ + { + "description": "request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.DatabaseDelete" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + }, + "x-panel-log": { + "BeforeFunctions": [ + { + "db": "databases", + "input_column": "id", + "input_value": "ids", + "isList": true, + "output_column": "name", + "output_value": "names" + } + ], + "bodyKeys": [ + "ids" + ], + "formatEN": "delete database [names]", + "formatZH": "删除远程数据库 [names]", + "paramKeys": [] + } + } + }, + "/databases/db/item/:type": { + "get": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "获取数据库列表", + "tags": [ + "Database" + ], + "summary": "List databases", + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/dto.DatabaseItem" + } + } + } + } + } + }, + "/databases/db/list/:type": { + "get": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "获取远程数据库列表", + "tags": [ + "Database" + ], + "summary": "List databases", + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/dto.DatabaseOption" + } + } + } + } + } + }, + "/databases/db/search": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "获取远程数据库列表分页", + "consumes": [ + "application/json" + ], + "tags": [ + "Database" + ], + "summary": "Page databases", + "parameters": [ + { + "description": "request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.DatabaseSearch" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/dto.PageResult" + } + } + } + } + }, + "/databases/db/update": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "更新远程数据库", + "consumes": [ + "application/json" + ], + "tags": [ + "Database" + ], + "summary": "Update database", + "parameters": [ + { + "description": "request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.DatabaseUpdate" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + }, + "x-panel-log": { + "BeforeFunctions": [], + "bodyKeys": [ + "name" + ], + "formatEN": "update database [name]", + "formatZH": "更新远程数据库 [name]", + "paramKeys": [] + } + } + }, + "/databases/del": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "删除 mysql 数据库", + "consumes": [ + "application/json" + ], + "tags": [ + "Database Mysql" + ], + "summary": "Delete mysql database", + "parameters": [ + { + "description": "request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.MysqlDBDelete" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + }, + "x-panel-log": { + "BeforeFunctions": [ + { + "db": "database_mysqls", + "input_column": "id", + "input_value": "id", + "isList": false, + "output_column": "name", + "output_value": "name" + } + ], + "bodyKeys": [ + "id" + ], + "formatEN": "delete mysql database [name]", + "formatZH": "删除 mysql 数据库 [name]", + "paramKeys": [] + } + } + }, + "/databases/del/check": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "Mysql 数据库删除前检查", + "consumes": [ + "application/json" + ], + "tags": [ + "Database Mysql" + ], + "summary": "Check before delete mysql database", + "parameters": [ + { + "description": "request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.MysqlDBDeleteCheck" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "array", + "items": { + "type": "string" + } + } + } + } + } + }, + "/databases/description/update": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "更新 mysql 数据库库描述信息", + "consumes": [ + "application/json" + ], + "tags": [ + "Database Mysql" + ], + "summary": "Update mysql database description", + "parameters": [ + { + "description": "request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.UpdateDescription" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + }, + "x-panel-log": { + "BeforeFunctions": [ + { + "db": "database_mysqls", + "input_column": "id", + "input_value": "id", + "isList": false, + "output_column": "name", + "output_value": "name" + } + ], + "bodyKeys": [ + "id", + "description" + ], + "formatEN": "The description of the mysql database [name] is modified =\u003e [description]", + "formatZH": "mysql 数据库 [name] 描述信息修改 [description]", + "paramKeys": [] + } + } + }, + "/databases/load": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "从服务器获取", + "consumes": [ + "application/json" + ], + "tags": [ + "Database Mysql" + ], + "summary": "Load mysql database from remote", + "parameters": [ + { + "description": "request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.MysqlLoadDB" + } + } + ], + "responses": {} + } + }, + "/databases/options": { + "get": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "获取 mysql 数据库列表", + "consumes": [ + "application/json" + ], + "tags": [ + "Database Mysql" + ], + "summary": "List mysql database names", + "parameters": [ + { + "description": "request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.PageInfo" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/dto.MysqlOption" + } + } + } + } + } + }, + "/databases/pg": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "创建 postgresql 数据库", + "consumes": [ + "application/json" + ], + "tags": [ + "Database Postgresql" + ], + "summary": "Create postgresql database", + "parameters": [ + { + "description": "request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.PostgresqlDBCreate" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + }, + "x-panel-log": { + "BeforeFunctions": [], + "bodyKeys": [ + "name" + ], + "formatEN": "create postgresql database [name]", + "formatZH": "创建 postgresql 数据库 [name]", + "paramKeys": [] + } + } + }, + "/databases/pg/:database/load": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "从服务器获取", + "consumes": [ + "application/json" + ], + "tags": [ + "Database Postgresql" + ], + "summary": "Load postgresql database from remote", + "parameters": [ + { + "description": "request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.PostgresqlLoadDB" + } + } + ], + "responses": {} + } + }, + "/databases/pg/bind": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "绑定 postgresql 数据库用户", + "consumes": [ + "application/json" + ], + "tags": [ + "Database Postgresql" + ], + "summary": "Bind postgresql user", + "parameters": [ + { + "description": "request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.PostgresqlBindUser" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + }, + "x-panel-log": { + "BeforeFunctions": [], + "bodyKeys": [ + "name", + "username" + ], + "formatEN": "bind postgresql database [name] user [username]", + "formatZH": "绑定 postgresql 数据库 [name] 用户 [username]", + "paramKeys": [] + } + } + }, + "/databases/pg/del": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "删除 postgresql 数据库", + "consumes": [ + "application/json" + ], + "tags": [ + "Database Postgresql" + ], + "summary": "Delete postgresql database", + "parameters": [ + { + "description": "request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.PostgresqlDBDelete" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + }, + "x-panel-log": { + "BeforeFunctions": [ + { + "db": "database_postgresqls", + "input_column": "id", + "input_value": "id", + "isList": false, + "output_column": "name", + "output_value": "name" + } + ], + "bodyKeys": [ + "id" + ], + "formatEN": "delete postgresql database [name]", + "formatZH": "删除 postgresql 数据库 [name]", + "paramKeys": [] + } + } + }, + "/databases/pg/del/check": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "Postgresql 数据库删除前检查", + "consumes": [ + "application/json" + ], + "tags": [ + "Database Postgresql" + ], + "summary": "Check before delete postgresql database", + "parameters": [ + { + "description": "request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.PostgresqlDBDeleteCheck" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "array", + "items": { + "type": "string" + } + } + } + } + } + }, + "/databases/pg/description": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "更新 postgresql 数据库库描述信息", + "consumes": [ + "application/json" + ], + "tags": [ + "Database Postgresql" + ], + "summary": "Update postgresql database description", + "parameters": [ + { + "description": "request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.UpdateDescription" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + }, + "x-panel-log": { + "BeforeFunctions": [ + { + "db": "database_postgresqls", + "input_column": "id", + "input_value": "id", + "isList": false, + "output_column": "name", + "output_value": "name" + } + ], + "bodyKeys": [ + "id", + "description" + ], + "formatEN": "The description of the postgresql database [name] is modified =\u003e [description]", + "formatZH": "postgresql 数据库 [name] 描述信息修改 [description]", + "paramKeys": [] + } + } + }, + "/databases/pg/password": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "修改 postgresql 密码", + "consumes": [ + "application/json" + ], + "tags": [ + "Database Postgresql" + ], + "summary": "Change postgresql password", + "parameters": [ + { + "description": "request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.ChangeDBInfo" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + }, + "x-panel-log": { + "BeforeFunctions": [ + { + "db": "database_postgresqls", + "input_column": "id", + "input_value": "id", + "isList": false, + "output_column": "name", + "output_value": "name" + } + ], + "bodyKeys": [ + "id" + ], + "formatEN": "Update database [name] password", + "formatZH": "更新数据库 [name] 密码", + "paramKeys": [] + } + } + }, + "/databases/pg/privileges": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "修改 postgresql 用户权限", + "consumes": [ + "application/json" + ], + "tags": [ + "Database Postgresql" + ], + "summary": "Change postgresql privileges", + "parameters": [ + { + "description": "request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.ChangeDBInfo" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + }, + "x-panel-log": { + "BeforeFunctions": [], + "bodyKeys": [ + "database", + "username" + ], + "formatEN": "Update [user] privileges of database [database]", + "formatZH": "更新数据库 [database] 用户 [username] 权限", + "paramKeys": [] + } + } + }, + "/databases/pg/search": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "获取 postgresql 数据库列表分页", + "consumes": [ + "application/json" + ], + "tags": [ + "Database Postgresql" + ], + "summary": "Page postgresql databases", + "parameters": [ + { + "description": "request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.PostgresqlDBSearch" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/dto.PageResult" + } + } + } + } + }, + "/databases/redis/conf": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "获取 redis 配置信息", + "consumes": [ + "application/json" + ], + "tags": [ + "Database Redis" + ], + "summary": "Load redis conf", + "parameters": [ + { + "description": "request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.OperationWithName" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/dto.RedisConf" + } + } + } + } + }, + "/databases/redis/conf/update": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "更新 redis 配置信息", + "consumes": [ + "application/json" + ], + "tags": [ + "Database Redis" + ], + "summary": "Update redis conf", + "parameters": [ + { + "description": "request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.RedisConfUpdate" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + }, + "x-panel-log": { + "BeforeFunctions": [], + "bodyKeys": [], + "formatEN": "update the redis database configuration information", + "formatZH": "更新 redis 数据库配置信息", + "paramKeys": [] + } + } + }, + "/databases/redis/install/cli": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "安装 redis cli", + "tags": [ + "Database Redis" + ], + "summary": "Install redis-cli", + "responses": { + "200": { + "description": "OK" + } + } + } + }, + "/databases/redis/password": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "更新 redis 密码", + "consumes": [ + "application/json" + ], + "tags": [ + "Database Redis" + ], + "summary": "Change redis password", + "parameters": [ + { + "description": "request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.ChangeRedisPass" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + }, + "x-panel-log": { + "BeforeFunctions": [], + "bodyKeys": [], + "formatEN": "change the password of the redis database", + "formatZH": "修改 redis 数据库密码", + "paramKeys": [] + } + } + }, + "/databases/redis/persistence/conf": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "获取 redis 持久化配置", + "consumes": [ + "application/json" + ], + "tags": [ + "Database Redis" + ], + "summary": "Load redis persistence conf", + "parameters": [ + { + "description": "request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.OperationWithName" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/dto.RedisPersistence" + } + } + } + } + }, + "/databases/redis/persistence/update": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "更新 redis 持久化配置", + "consumes": [ + "application/json" + ], + "tags": [ + "Database Redis" + ], + "summary": "Update redis persistence conf", + "parameters": [ + { + "description": "request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.RedisConfPersistenceUpdate" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + }, + "x-panel-log": { + "BeforeFunctions": [], + "bodyKeys": [], + "formatEN": "redis database persistence configuration update", + "formatZH": "redis 数据库持久化配置更新", + "paramKeys": [] + } + } + }, + "/databases/redis/status": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "获取 redis 状态信息", + "consumes": [ + "application/json" + ], + "tags": [ + "Database Redis" + ], + "summary": "Load redis status info", + "parameters": [ + { + "description": "request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.OperationWithName" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/dto.RedisStatus" + } + } + } + } + }, + "/databases/remote": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "获取 mysql 远程访问权限", + "consumes": [ + "application/json" + ], + "tags": [ + "Database Mysql" + ], + "summary": "Load mysql remote access", + "parameters": [ + { + "description": "request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.OperationWithNameAndType" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "boolean" + } + } + } + } + }, + "/databases/search": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "获取 mysql 数据库列表分页", + "consumes": [ + "application/json" + ], + "tags": [ + "Database Mysql" + ], + "summary": "Page mysql databases", + "parameters": [ + { + "description": "request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.MysqlDBSearch" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/dto.PageResult" + } + } + } + } + }, + "/databases/status": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "获取 mysql 状态信息", + "consumes": [ + "application/json" + ], + "tags": [ + "Database Mysql" + ], + "summary": "Load mysql status info", + "parameters": [ + { + "description": "request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.OperationWithNameAndType" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/dto.MysqlStatus" + } + } + } + } + }, + "/databases/variables": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "获取 mysql 性能参数信息", + "consumes": [ + "application/json" + ], + "tags": [ + "Database Mysql" + ], + "summary": "Load mysql variables info", + "parameters": [ + { + "description": "request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.OperationWithNameAndType" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/dto.MysqlVariables" + } + } + } + } + }, + "/databases/variables/update": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "mysql 性能调优", + "consumes": [ + "application/json" + ], + "tags": [ + "Database Mysql" + ], + "summary": "Update mysql variables", + "parameters": [ + { + "description": "request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.MysqlVariablesUpdate" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + }, + "x-panel-log": { + "BeforeFunctions": [], + "bodyKeys": [], + "formatEN": "adjust mysql database performance parameters", + "formatZH": "调整 mysql 数据库性能参数", + "paramKeys": [] + } + } + }, + "/db/remote/del/check": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "Mysql 远程数据库删除前检查", + "consumes": [ + "application/json" + ], + "tags": [ + "Database" + ], + "summary": "Check before delete remote database", + "parameters": [ + { + "description": "request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.OperateByID" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "array", + "items": { + "type": "string" + } + } + } + } + } + }, + "/files": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "创建文件/文件夹", + "consumes": [ + "application/json" + ], + "tags": [ + "File" + ], + "summary": "Create file", + "parameters": [ + { + "description": "request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/request.FileCreate" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + }, + "x-panel-log": { + "BeforeFunctions": [], + "bodyKeys": [ + "path" + ], + "formatEN": "Create dir or file [path]", + "formatZH": "创建文件/文件夹 [path]", + "paramKeys": [] + } + } + }, + "/files/batch/del": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "批量删除文件/文件夹", + "consumes": [ + "application/json" + ], + "tags": [ + "File" + ], + "summary": "Batch delete file", + "parameters": [ + { + "description": "request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/request.FileBatchDelete" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + }, + "x-panel-log": { + "BeforeFunctions": [], + "bodyKeys": [ + "paths" + ], + "formatEN": "Batch delete dir or file [paths]", + "formatZH": "批量删除文件/文件夹 [paths]", + "paramKeys": [] + } + } + }, + "/files/batch/role": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "批量修改文件权限和用户/组", + "consumes": [ + "application/json" + ], + "tags": [ + "File" + ], + "summary": "Batch change file mode and owner", + "parameters": [ + { + "description": "request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/request.FileRoleReq" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + }, + "x-panel-log": { + "BeforeFunctions": [], + "bodyKeys": [ + "paths", + "mode", + "user", + "group" + ], + "formatEN": "Batch change file mode and owner [paths] =\u003e [mode]/[user]/[group]", + "formatZH": "批量修改文件权限和用户/组 [paths] =\u003e [mode]/[user]/[group]", + "paramKeys": [] + } + } + }, + "/files/check": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "检测文件是否存在", + "consumes": [ + "application/json" + ], + "tags": [ + "File" + ], + "summary": "Check file exist", + "parameters": [ + { + "description": "request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/request.FilePathCheck" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + } + } + }, + "/files/chunkdownload": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "分片下载下载文件", + "consumes": [ + "application/json" + ], + "tags": [ + "File" + ], + "summary": "Chunk Download file", + "parameters": [ + { + "description": "request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/request.FileDownload" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + }, + "x-panel-log": { + "BeforeFunctions": [], + "bodyKeys": [ + "name" + ], + "formatEN": "Download file [name]", + "formatZH": "下载文件 [name]", + "paramKeys": [] + } + } + }, + "/files/chunkupload": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "分片上传文件", + "tags": [ + "File" + ], + "summary": "ChunkUpload file", + "parameters": [ + { + "type": "file", + "description": "request", + "name": "file", + "in": "formData", + "required": true + } + ], + "responses": { + "200": { + "description": "OK" + } + } + } + }, + "/files/compress": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "压缩文件", + "consumes": [ + "application/json" + ], + "tags": [ + "File" + ], + "summary": "Compress file", + "parameters": [ + { + "description": "request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/request.FileCompress" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + }, + "x-panel-log": { + "BeforeFunctions": [], + "bodyKeys": [ + "name" + ], + "formatEN": "Compress file [name]", + "formatZH": "压缩文件 [name]", + "paramKeys": [] + } + } + }, + "/files/content": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "获取文件内容", + "consumes": [ + "application/json" + ], + "tags": [ + "File" + ], + "summary": "Load file content", + "parameters": [ + { + "description": "request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/request.FileContentReq" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/response.FileInfo" + } + } + }, + "x-panel-log": { + "BeforeFunctions": [], + "bodyKeys": [ + "path" + ], + "formatEN": "Load file content [path]", + "formatZH": "获取文件内容 [path]", + "paramKeys": [] + } + } + }, + "/files/decompress": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "解压文件", + "consumes": [ + "application/json" + ], + "tags": [ + "File" + ], + "summary": "Decompress file", + "parameters": [ + { + "description": "request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/request.FileDeCompress" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + }, + "x-panel-log": { + "BeforeFunctions": [], + "bodyKeys": [ + "path" + ], + "formatEN": "Decompress file [path]", + "formatZH": "解压 [path]", + "paramKeys": [] + } + } + }, + "/files/del": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "删除文件/文件夹", + "consumes": [ + "application/json" + ], + "tags": [ + "File" + ], + "summary": "Delete file", + "parameters": [ + { + "description": "request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/request.FileDelete" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + }, + "x-panel-log": { + "BeforeFunctions": [], + "bodyKeys": [ + "path" + ], + "formatEN": "Delete dir or file [path]", + "formatZH": "删除文件/文件夹 [path]", + "paramKeys": [] + } + } + }, + "/files/download": { + "get": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "下载文件", + "consumes": [ + "application/json" + ], + "tags": [ + "File" + ], + "summary": "Download file", + "responses": { + "200": { + "description": "OK" + } + } + } + }, + "/files/favorite": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "创建收藏", + "consumes": [ + "application/json" + ], + "tags": [ + "File" + ], + "summary": "Create favorite", + "parameters": [ + { + "description": "request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/request.FavoriteCreate" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + }, + "x-panel-log": { + "BeforeFunctions": [], + "bodyKeys": [ + "path" + ], + "formatEN": "收藏文件/文件夹 [path]", + "formatZH": "收藏文件/文件夹 [path]", + "paramKeys": [] + } + } + }, + "/files/favorite/del": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "删除收藏", + "consumes": [ + "application/json" + ], + "tags": [ + "File" + ], + "summary": "Delete favorite", + "parameters": [ + { + "description": "request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/request.FavoriteDelete" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + }, + "x-panel-log": { + "BeforeFunctions": [ + { + "db": "favorites", + "input_column": "id", + "input_value": "id", + "isList": false, + "output_column": "path", + "output_value": "path" + } + ], + "bodyKeys": [ + "id" + ], + "formatEN": "delete avorite [path]", + "formatZH": "删除收藏 [path]", + "paramKeys": [] + } + } + }, + "/files/favorite/search": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "获取收藏列表", + "consumes": [ + "application/json" + ], + "tags": [ + "File" + ], + "summary": "List favorites", + "parameters": [ + { + "description": "request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.PageInfo" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + } + } + }, + "/files/mode": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "修改文件权限", + "consumes": [ + "application/json" + ], + "tags": [ + "File" + ], + "summary": "Change file mode", + "parameters": [ + { + "description": "request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/request.FileCreate" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + }, + "x-panel-log": { + "BeforeFunctions": [], + "bodyKeys": [ + "path", + "mode" + ], + "formatEN": "Change mode [paths] =\u003e [mode]", + "formatZH": "修改权限 [paths] =\u003e [mode]", + "paramKeys": [] + } + } + }, + "/files/move": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "移动文件", + "consumes": [ + "application/json" + ], + "tags": [ + "File" + ], + "summary": "Move file", + "parameters": [ + { + "description": "request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/request.FileMove" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + }, + "x-panel-log": { + "BeforeFunctions": [], + "bodyKeys": [ + "oldPaths", + "newPath" + ], + "formatEN": "Move [oldPaths] =\u003e [newPath]", + "formatZH": "移动文件 [oldPaths] =\u003e [newPath]", + "paramKeys": [] + } + } + }, + "/files/owner": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "修改文件用户/组", + "consumes": [ + "application/json" + ], + "tags": [ + "File" + ], + "summary": "Change file owner", + "parameters": [ + { + "description": "request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/request.FileRoleUpdate" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + }, + "x-panel-log": { + "BeforeFunctions": [], + "bodyKeys": [ + "path", + "user", + "group" + ], + "formatEN": "Change owner [paths] =\u003e [user]/[group]", + "formatZH": "修改用户/组 [paths] =\u003e [user]/[group]", + "paramKeys": [] + } + } + }, + "/files/read": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "按行读取日志文件", + "tags": [ + "File" + ], + "summary": "Read file by Line", + "parameters": [ + { + "description": "request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/request.FileReadByLineReq" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + } + } + }, + "/files/recycle/clear": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "清空回收站文件", + "consumes": [ + "application/json" + ], + "tags": [ + "File" + ], + "summary": "Clear RecycleBin files", + "responses": { + "200": { + "description": "OK" + } + }, + "x-panel-log": { + "BeforeFunctions": [], + "bodyKeys": [], + "formatEN": "清空回收站", + "formatZH": "清空回收站", + "paramKeys": [] + } + } + }, + "/files/recycle/reduce": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "还原回收站文件", + "consumes": [ + "application/json" + ], + "tags": [ + "File" + ], + "summary": "Reduce RecycleBin files", + "parameters": [ + { + "description": "request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/request.RecycleBinReduce" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + }, + "x-panel-log": { + "BeforeFunctions": [], + "bodyKeys": [ + "name" + ], + "formatEN": "Reduce RecycleBin file [name]", + "formatZH": "还原回收站文件 [name]", + "paramKeys": [] + } + } + }, + "/files/recycle/search": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "获取回收站文件列表", + "consumes": [ + "application/json" + ], + "tags": [ + "File" + ], + "summary": "List RecycleBin files", + "parameters": [ + { + "description": "request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.PageInfo" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + } + } + }, + "/files/recycle/status": { + "get": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "获取回收站状态", + "consumes": [ + "application/json" + ], + "tags": [ + "File" + ], + "summary": "Get RecycleBin status", + "responses": { + "200": { + "description": "OK" + } + } + } + }, + "/files/rename": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "修改文件名称", + "consumes": [ + "application/json" + ], + "tags": [ + "File" + ], + "summary": "Change file name", + "parameters": [ + { + "description": "request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/request.FileRename" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + }, + "x-panel-log": { + "BeforeFunctions": [], + "bodyKeys": [ + "oldName", + "newName" + ], + "formatEN": "Rename [oldName] =\u003e [newName]", + "formatZH": "重命名 [oldName] =\u003e [newName]", + "paramKeys": [] + } + } + }, + "/files/save": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "更新文件内容", + "consumes": [ + "application/json" + ], + "tags": [ + "File" + ], + "summary": "Update file content", + "parameters": [ + { + "description": "request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/request.FileEdit" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + }, + "x-panel-log": { + "BeforeFunctions": [], + "bodyKeys": [ + "path" + ], + "formatEN": "Update file content [path]", + "formatZH": "更新文件内容 [path]", + "paramKeys": [] + } + } + }, + "/files/search": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "获取文件列表", + "consumes": [ + "application/json" + ], + "tags": [ + "File" + ], + "summary": "List files", + "parameters": [ + { + "description": "request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/request.FileOption" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/response.FileInfo" + } + } + } + } + }, + "/files/size": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "获取文件夹大小", + "consumes": [ + "application/json" + ], + "tags": [ + "File" + ], + "summary": "Load file size", + "parameters": [ + { + "description": "request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/request.DirSizeReq" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + }, + "x-panel-log": { + "BeforeFunctions": [], + "bodyKeys": [ + "path" + ], + "formatEN": "Load file size [path]", + "formatZH": "获取文件夹大小 [path]", + "paramKeys": [] + } + } + }, + "/files/tree": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "加载文件树", + "consumes": [ + "application/json" + ], + "tags": [ + "File" + ], + "summary": "Load files tree", + "parameters": [ + { + "description": "request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/request.FileOption" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/response.FileTree" + } + } + } + } + } + }, + "/files/upload": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "上传文件", + "tags": [ + "File" + ], + "summary": "Upload file", + "parameters": [ + { + "type": "file", + "description": "request", + "name": "file", + "in": "formData", + "required": true + } + ], + "responses": { + "200": { + "description": "OK" + } + }, + "x-panel-log": { + "BeforeFunctions": [], + "bodyKeys": [ + "path" + ], + "formatEN": "Upload file [path]", + "formatZH": "上传文件 [path]", + "paramKeys": [] + } + } + }, + "/files/upload/search": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "分页获取上传文件", + "consumes": [ + "application/json" + ], + "tags": [ + "File" + ], + "summary": "Page file", + "parameters": [ + { + "description": "request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/request.SearchUploadWithPage" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/response.FileInfo" + } + } + } + } + } + }, + "/files/wget": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "下载远端文件", + "consumes": [ + "application/json" + ], + "tags": [ + "File" + ], + "summary": "Wget file", + "parameters": [ + { + "description": "request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/request.FileWget" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + }, + "x-panel-log": { + "BeforeFunctions": [], + "bodyKeys": [ + "url", + "path", + "name" + ], + "formatEN": "Download url =\u003e [path]/[name]", + "formatZH": "下载 url =\u003e [path]/[name]", + "paramKeys": [] + } + } + }, + "/groups": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "创建系统组", + "consumes": [ + "application/json" + ], + "tags": [ + "System Group" + ], + "summary": "Create group", + "parameters": [ + { + "description": "request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.GroupCreate" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + }, + "x-panel-log": { + "BeforeFunctions": [], + "bodyKeys": [ + "name", + "type" + ], + "formatEN": "create group [name][type]", + "formatZH": "创建组 [name][type]", + "paramKeys": [] + } + } + }, + "/groups/del": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "删除系统组", + "consumes": [ + "application/json" + ], + "tags": [ + "System Group" + ], + "summary": "Delete group", + "parameters": [ + { + "description": "request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.OperateByID" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + }, + "x-panel-log": { + "BeforeFunctions": [ + { + "db": "groups", + "input_column": "id", + "input_value": "id", + "isList": false, + "output_column": "name", + "output_value": "name" + }, + { + "db": "groups", + "input_column": "id", + "input_value": "id", + "isList": false, + "output_column": "type", + "output_value": "type" + } + ], + "bodyKeys": [ + "id" + ], + "formatEN": "delete group [type][name]", + "formatZH": "删除组 [type][name]", + "paramKeys": [] + } + } + }, + "/groups/search": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "查询系统组", + "consumes": [ + "application/json" + ], + "tags": [ + "System Group" + ], + "summary": "List groups", + "parameters": [ + { + "description": "request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.GroupSearch" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/dto.GroupInfo" + } + } + } + } + } + }, + "/groups/update": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "更新系统组", + "consumes": [ + "application/json" + ], + "tags": [ + "System Group" + ], + "summary": "Update group", + "parameters": [ + { + "description": "request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.GroupUpdate" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + }, + "x-panel-log": { + "BeforeFunctions": [], + "bodyKeys": [ + "name", + "type" + ], + "formatEN": "update group [name][type]", + "formatZH": "更新组 [name][type]", + "paramKeys": [] + } + } + }, + "/host/conffile/update": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "上传文件更新 SSH 配置", + "consumes": [ + "application/json" + ], + "tags": [ + "SSH" + ], + "summary": "Update host SSH setting by file", + "parameters": [ + { + "description": "request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.SSHConf" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + }, + "x-panel-log": { + "BeforeFunctions": [], + "bodyKeys": [], + "formatEN": "update SSH conf", + "formatZH": "修改 SSH 配置文件", + "paramKeys": [] + } + } + }, + "/host/ssh/conf": { + "get": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "获取 SSH 配置文件", + "tags": [ + "SSH" + ], + "summary": "Load host SSH conf", + "responses": { + "200": { + "description": "OK" + } + } + } + }, + "/host/ssh/generate": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "生成 SSH 密钥", + "consumes": [ + "application/json" + ], + "tags": [ + "SSH" + ], + "summary": "Generate host SSH secret", + "parameters": [ + { + "description": "request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.GenerateSSH" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + }, + "x-panel-log": { + "BeforeFunctions": [], + "bodyKeys": [], + "formatEN": "generate SSH secret", + "formatZH": "生成 SSH 密钥 ", + "paramKeys": [] + } + } + }, + "/host/ssh/log": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "获取 SSH 登录日志", + "consumes": [ + "application/json" + ], + "tags": [ + "SSH" + ], + "summary": "Load host SSH logs", + "parameters": [ + { + "description": "request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.SearchSSHLog" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/dto.SSHLog" + } + } + } + } + }, + "/host/ssh/operate": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "修改 SSH 服务状态", + "consumes": [ + "application/json" + ], + "tags": [ + "SSH" + ], + "summary": "Operate SSH", + "parameters": [ + { + "description": "request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.Operate" + } + } + ], + "responses": {}, + "x-panel-log": { + "BeforeFunctions": [], + "bodyKeys": [ + "operation" + ], + "formatEN": "[operation] SSH", + "formatZH": "[operation] SSH ", + "paramKeys": [] + } + } + }, + "/host/ssh/search": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "加载 SSH 配置信息", + "tags": [ + "SSH" + ], + "summary": "Load host SSH setting info", + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/dto.SSHInfo" + } + } + } + } + }, + "/host/ssh/secret": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "获取 SSH 密钥", + "consumes": [ + "application/json" + ], + "tags": [ + "SSH" + ], + "summary": "Load host SSH secret", + "parameters": [ + { + "description": "request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.GenerateLoad" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + } + } + }, + "/host/ssh/update": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "更新 SSH 配置", + "consumes": [ + "application/json" + ], + "tags": [ + "SSH" + ], + "summary": "Update host SSH setting", + "parameters": [ + { + "description": "request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.SSHUpdate" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + }, + "x-panel-log": { + "BeforeFunctions": [], + "bodyKeys": [ + "key", + "value" + ], + "formatEN": "update SSH setting [key] =\u003e [value]", + "formatZH": "修改 SSH 配置 [key] =\u003e [value]", + "paramKeys": [] + } + } + }, + "/host/tool": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "获取主机工具状态", + "consumes": [ + "application/json" + ], + "tags": [ + "Host tool" + ], + "summary": "Get tool", + "parameters": [ + { + "description": "request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/request.HostToolReq" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + } + } + }, + "/host/tool/config": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "操作主机工具配置文件", + "consumes": [ + "application/json" + ], + "tags": [ + "Host tool" + ], + "summary": "Get tool config", + "parameters": [ + { + "description": "request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/request.HostToolConfig" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + }, + "x-panel-log": { + "BeforeFunctions": [], + "bodyKeys": [ + "operate" + ], + "formatEN": "[operate] tool config", + "formatZH": "[operate] 主机工具配置文件 ", + "paramKeys": [] + } + } + }, + "/host/tool/create": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "创建主机工具配置", + "consumes": [ + "application/json" + ], + "tags": [ + "Host tool" + ], + "summary": "Create Host tool Config", + "parameters": [ + { + "description": "request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/request.HostToolCreate" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + }, + "x-panel-log": { + "BeforeFunctions": [], + "bodyKeys": [ + "type" + ], + "formatEN": "create [type] config", + "formatZH": "创建 [type] 配置", + "paramKeys": [] + } + } + }, + "/host/tool/log": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "获取主机工具日志", + "consumes": [ + "application/json" + ], + "tags": [ + "Host tool" + ], + "summary": "Get tool", + "parameters": [ + { + "description": "request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/request.HostToolLogReq" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + } + } + }, + "/host/tool/operate": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "操作主机工具", + "consumes": [ + "application/json" + ], + "tags": [ + "Host tool" + ], + "summary": "Operate tool", + "parameters": [ + { + "description": "request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/request.HostToolReq" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + }, + "x-panel-log": { + "BeforeFunctions": [], + "bodyKeys": [ + "operate", + "type" + ], + "formatEN": "[operate] [type]", + "formatZH": "[operate] [type] ", + "paramKeys": [] + } + } + }, + "/host/tool/supervisor/process": { + "get": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "获取 Supervisor 进程配置", + "consumes": [ + "application/json" + ], + "tags": [ + "Host tool" + ], + "summary": "Get Supervisor process config", + "responses": { + "200": { + "description": "OK" + } + } + }, + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "操作守护进程", + "consumes": [ + "application/json" + ], + "tags": [ + "Host tool" + ], + "summary": "Create Supervisor process", + "parameters": [ + { + "description": "request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/request.SupervisorProcessConfig" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + }, + "x-panel-log": { + "BeforeFunctions": [], + "bodyKeys": [ + "operate" + ], + "formatEN": "[operate] process", + "formatZH": "[operate] 守护进程 ", + "paramKeys": [] + } + } + }, + "/host/tool/supervisor/process/file": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "操作 Supervisor 进程文件", + "consumes": [ + "application/json" + ], + "tags": [ + "Host tool" + ], + "summary": "Get Supervisor process config", + "parameters": [ + { + "description": "request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/request.SupervisorProcessFileReq" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + }, + "x-panel-log": { + "BeforeFunctions": [], + "bodyKeys": [ + "operate" + ], + "formatEN": "[operate] Supervisor Process Config file", + "formatZH": "[operate] Supervisor 进程文件 ", + "paramKeys": [] + } + } + }, + "/hosts": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "创建主机", + "consumes": [ + "application/json" + ], + "tags": [ + "Host" + ], + "summary": "Create host", + "parameters": [ + { + "description": "request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.HostOperate" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + }, + "x-panel-log": { + "BeforeFunctions": [], + "bodyKeys": [ + "name", + "addr" + ], + "formatEN": "create host [name][addr]", + "formatZH": "创建主机 [name][addr]", + "paramKeys": [] + } + } + }, + "/hosts/command": { + "get": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "获取快速命令列表", + "tags": [ + "Command" + ], + "summary": "List commands", + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/dto.CommandInfo" + } + } + } + }, + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "创建快速命令", + "consumes": [ + "application/json" + ], + "tags": [ + "Command" + ], + "summary": "Create command", + "parameters": [ + { + "description": "request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.CommandOperate" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + }, + "x-panel-log": { + "BeforeFunctions": [], + "bodyKeys": [ + "name", + "command" + ], + "formatEN": "create quick command [name][command]", + "formatZH": "创建快捷命令 [name][command]", + "paramKeys": [] + } + } + }, + "/hosts/command/del": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "删除快速命令", + "consumes": [ + "application/json" + ], + "tags": [ + "Command" + ], + "summary": "Delete command", + "parameters": [ + { + "description": "request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.BatchDeleteReq" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + }, + "x-panel-log": { + "BeforeFunctions": [ + { + "db": "commands", + "input_column": "id", + "input_value": "ids", + "isList": true, + "output_column": "name", + "output_value": "names" + } + ], + "bodyKeys": [ + "ids" + ], + "formatEN": "delete quick command [names]", + "formatZH": "删除快捷命令 [names]", + "paramKeys": [] + } + } + }, + "/hosts/command/redis": { + "get": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "获取 redis 快速命令列表", + "tags": [ + "Redis Command" + ], + "summary": "List redis commands", + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "Array" + } + } + } + }, + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "保存 Redis 快速命令", + "consumes": [ + "application/json" + ], + "tags": [ + "Redis Command" + ], + "summary": "Save redis command", + "parameters": [ + { + "description": "request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.RedisCommand" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + }, + "x-panel-log": { + "BeforeFunctions": [], + "bodyKeys": [ + "name", + "command" + ], + "formatEN": "save quick command for redis [name][command]", + "formatZH": "保存 redis 快捷命令 [name][command]", + "paramKeys": [] + } + } + }, + "/hosts/command/redis/del": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "删除 redis 快速命令", + "consumes": [ + "application/json" + ], + "tags": [ + "Redis Command" + ], + "summary": "Delete redis command", + "parameters": [ + { + "description": "request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.BatchDeleteReq" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + }, + "x-panel-log": { + "BeforeFunctions": [ + { + "db": "redis_commands", + "input_column": "id", + "input_value": "ids", + "isList": true, + "output_column": "name", + "output_value": "names" + } + ], + "bodyKeys": [ + "ids" + ], + "formatEN": "delete quick command of redis [names]", + "formatZH": "删除 redis 快捷命令 [names]", + "paramKeys": [] + } + } + }, + "/hosts/command/redis/search": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "获取 redis 快速命令列表分页", + "consumes": [ + "application/json" + ], + "tags": [ + "Redis Command" + ], + "summary": "Page redis commands", + "parameters": [ + { + "description": "request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.SearchWithPage" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/dto.PageResult" + } + } + } + } + }, + "/hosts/command/search": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "获取快速命令列表分页", + "consumes": [ + "application/json" + ], + "tags": [ + "Command" + ], + "summary": "Page commands", + "parameters": [ + { + "description": "request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.SearchWithPage" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/dto.PageResult" + } + } + } + } + }, + "/hosts/command/tree": { + "get": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "获取快速命令树", + "consumes": [ + "application/json" + ], + "tags": [ + "Command" + ], + "summary": "Tree commands", + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "Array" + } + } + } + } + }, + "/hosts/command/update": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "更新快速命令", + "consumes": [ + "application/json" + ], + "tags": [ + "Command" + ], + "summary": "Update command", + "parameters": [ + { + "description": "request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.CommandOperate" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + }, + "x-panel-log": { + "BeforeFunctions": [], + "bodyKeys": [ + "name" + ], + "formatEN": "update quick command [name]", + "formatZH": "更新快捷命令 [name]", + "paramKeys": [] + } + } + }, + "/hosts/del": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "删除主机", + "consumes": [ + "application/json" + ], + "tags": [ + "Host" + ], + "summary": "Delete host", + "parameters": [ + { + "description": "request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.BatchDeleteReq" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + }, + "x-panel-log": { + "BeforeFunctions": [ + { + "db": "hosts", + "input_column": "id", + "input_value": "ids", + "isList": true, + "output_column": "addr", + "output_value": "addrs" + } + ], + "bodyKeys": [ + "ids" + ], + "formatEN": "delete host [addrs]", + "formatZH": "删除主机 [addrs]", + "paramKeys": [] + } + } + }, + "/hosts/firewall/base": { + "get": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "获取防火墙基础信息", + "tags": [ + "Firewall" + ], + "summary": "Load firewall base info", + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/dto.FirewallBaseInfo" + } + } + } + } + }, + "/hosts/firewall/batch": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "批量删除防火墙规则", + "consumes": [ + "application/json" + ], + "tags": [ + "Firewall" + ], + "summary": "Create group", + "parameters": [ + { + "description": "request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.BatchRuleOperate" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + } + } + }, + "/hosts/firewall/forward": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "更新防火墙端口转发规则", + "consumes": [ + "application/json" + ], + "tags": [ + "Firewall" + ], + "summary": "Create group", + "parameters": [ + { + "description": "request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.ForwardRuleOperate" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + }, + "x-panel-log": { + "BeforeFunctions": [], + "bodyKeys": [ + "source_port" + ], + "formatEN": "update port forward rules [source_port]", + "formatZH": "更新端口转发规则 [source_port]", + "paramKeys": [] + } + } + }, + "/hosts/firewall/ip": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "创建防火墙 IP 规则", + "consumes": [ + "application/json" + ], + "tags": [ + "Firewall" + ], + "summary": "Create group", + "parameters": [ + { + "description": "request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.AddrRuleOperate" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + }, + "x-panel-log": { + "BeforeFunctions": [], + "bodyKeys": [ + "strategy", + "address" + ], + "formatEN": "create address rules [strategy][address]", + "formatZH": "添加 ip 规则 [strategy] [address]", + "paramKeys": [] + } + } + }, + "/hosts/firewall/operate": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "修改防火墙状态", + "consumes": [ + "application/json" + ], + "tags": [ + "Firewall" + ], + "summary": "Page firewall status", + "parameters": [ + { + "description": "request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.FirewallOperation" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/dto.PageResult" + } + } + }, + "x-panel-log": { + "BeforeFunctions": [], + "bodyKeys": [ + "operation" + ], + "formatEN": "[operation] firewall", + "formatZH": "[operation] 防火墙", + "paramKeys": [] + } + } + }, + "/hosts/firewall/port": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "创建防火墙端口规则", + "consumes": [ + "application/json" + ], + "tags": [ + "Firewall" + ], + "summary": "Create group", + "parameters": [ + { + "description": "request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.PortRuleOperate" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + }, + "x-panel-log": { + "BeforeFunctions": [], + "bodyKeys": [ + "port", + "strategy" + ], + "formatEN": "create port rules [strategy][port]", + "formatZH": "添加端口规则 [strategy] [port]", + "paramKeys": [] + } + } + }, + "/hosts/firewall/search": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "获取防火墙规则列表分页", + "consumes": [ + "application/json" + ], + "tags": [ + "Firewall" + ], + "summary": "Page firewall rules", + "parameters": [ + { + "description": "request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.RuleSearch" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/dto.PageResult" + } + } + } + } + }, + "/hosts/firewall/update/addr": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "更新 ip 防火墙规则", + "consumes": [ + "application/json" + ], + "tags": [ + "Firewall" + ], + "summary": "Create group", + "parameters": [ + { + "description": "request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.AddrRuleUpdate" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + } + } + }, + "/hosts/firewall/update/description": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "更新防火墙描述", + "consumes": [ + "application/json" + ], + "tags": [ + "Firewall" + ], + "summary": "Update rule description", + "parameters": [ + { + "description": "request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.UpdateFirewallDescription" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + } + } + }, + "/hosts/firewall/update/port": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "更新端口防火墙规则", + "consumes": [ + "application/json" + ], + "tags": [ + "Firewall" + ], + "summary": "Create group", + "parameters": [ + { + "description": "request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.PortRuleUpdate" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + } + } + }, + "/hosts/monitor/clean": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "清空监控数据", + "tags": [ + "Monitor" + ], + "summary": "Clean monitor datas", + "responses": { + "200": { + "description": "OK" + } + }, + "x-panel-log": { + "BeforeFunctions": [], + "bodyKeys": [], + "formatEN": "clean monitor datas", + "formatZH": "清空监控数据", + "paramKeys": [] + } + } + }, + "/hosts/monitor/search": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "获取监控数据", + "tags": [ + "Monitor" + ], + "summary": "Load monitor datas", + "parameters": [ + { + "description": "request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.MonitorSearch" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + } + } + }, + "/hosts/search": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "获取主机列表分页", + "consumes": [ + "application/json" + ], + "tags": [ + "Host" + ], + "summary": "Page host", + "parameters": [ + { + "description": "request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.SearchHostWithPage" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/dto.HostTree" + } + } + } + } + } + }, + "/hosts/test/byid/:id": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "测试主机连接", + "consumes": [ + "application/json" + ], + "tags": [ + "Host" + ], + "summary": "Test host conn by host id", + "parameters": [ + { + "type": "integer", + "description": "request", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "boolean" + } + } + } + } + }, + "/hosts/test/byinfo": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "测试主机连接", + "consumes": [ + "application/json" + ], + "tags": [ + "Host" + ], + "summary": "Test host conn by info", + "parameters": [ + { + "description": "request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.HostConnTest" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + } + } + }, + "/hosts/tree": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "加载主机树", + "consumes": [ + "application/json" + ], + "tags": [ + "Host" + ], + "summary": "Load host tree", + "parameters": [ + { + "description": "request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.SearchForTree" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/dto.HostTree" + } + } + } + } + } + }, + "/hosts/update": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "更新主机", + "consumes": [ + "application/json" + ], + "tags": [ + "Host" + ], + "summary": "Update host", + "parameters": [ + { + "description": "request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.HostOperate" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + }, + "x-panel-log": { + "BeforeFunctions": [], + "bodyKeys": [ + "name", + "addr" + ], + "formatEN": "update host [name][addr]", + "formatZH": "更新主机信息 [name][addr]", + "paramKeys": [] + } + } + }, + "/hosts/update/group": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "切换分组", + "consumes": [ + "application/json" + ], + "tags": [ + "Host" + ], + "summary": "Update host group", + "parameters": [ + { + "description": "request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.ChangeHostGroup" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + }, + "x-panel-log": { + "BeforeFunctions": [ + { + "db": "hosts", + "input_column": "id", + "input_value": "id", + "isList": false, + "output_column": "addr", + "output_value": "addr" + } + ], + "bodyKeys": [ + "id", + "group" + ], + "formatEN": "change host [addr] group =\u003e [group]", + "formatZH": "切换主机[addr]分组 =\u003e [group]", + "paramKeys": [] + } + } + }, + "/logs/clean": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "清空操作日志", + "consumes": [ + "application/json" + ], + "tags": [ + "Logs" + ], + "summary": "Clean operation logs", + "parameters": [ + { + "description": "request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.CleanLog" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/dto.PageResult" + } + } + }, + "x-panel-log": { + "BeforeFunctions": [], + "bodyKeys": [ + "logType" + ], + "formatEN": "Clean the [logType] log information", + "formatZH": "清空 [logType] 日志信息", + "paramKeys": [] + } + } + }, + "/logs/login": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "获取系统登录日志列表分页", + "consumes": [ + "application/json" + ], + "tags": [ + "Logs" + ], + "summary": "Page login logs", + "parameters": [ + { + "description": "request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.SearchLgLogWithPage" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/dto.PageResult" + } + } + } + } + }, + "/logs/operation": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "获取系统操作日志列表分页", + "consumes": [ + "application/json" + ], + "tags": [ + "Logs" + ], + "summary": "Page operation logs", + "parameters": [ + { + "description": "request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.SearchOpLogWithPage" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/dto.PageResult" + } + } + } + } + }, + "/logs/system": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "获取系统日志", + "tags": [ + "Logs" + ], + "summary": "Load system logs", + "responses": { + "200": { + "description": "OK" + } + } + } + }, + "/logs/system/files": { + "get": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "获取系统日志文件列表", + "tags": [ + "Logs" + ], + "summary": "Load system log files", + "responses": { + "200": { + "description": "OK" + } + } + } + }, + "/openresty": { + "get": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "获取 OpenResty 配置信息", + "tags": [ + "OpenResty" + ], + "summary": "Load OpenResty conf", + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/response.FileInfo" + } + } + } + } + }, + "/openresty/clear": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "清理 OpenResty 代理缓存", + "tags": [ + "OpenResty" + ], + "summary": "Clear OpenResty proxy cache", + "responses": { + "200": { + "description": "OK" + } + }, + "x-panel-log": { + "BeforeFunctions": [], + "bodyKeys": [], + "formatEN": "Clear nginx proxy cache", + "formatZH": "清理 Openresty 代理缓存", + "paramKeys": [] + } + } + }, + "/openresty/file": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "上传更新 OpenResty 配置文件", + "consumes": [ + "application/json" + ], + "tags": [ + "OpenResty" + ], + "summary": "Update OpenResty conf by upload file", + "parameters": [ + { + "description": "request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/request.NginxConfigFileUpdate" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + }, + "x-panel-log": { + "BeforeFunctions": [], + "bodyKeys": [], + "formatEN": "Update nginx conf", + "formatZH": "更新 nginx 配置", + "paramKeys": [] + } + } + }, + "/openresty/scope": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "获取部分 OpenResty 配置信息", + "consumes": [ + "application/json" + ], + "tags": [ + "OpenResty" + ], + "summary": "Load partial OpenResty conf", + "parameters": [ + { + "description": "request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/request.NginxScopeReq" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/response.NginxParam" + } + } + } + } + } + }, + "/openresty/status": { + "get": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "获取 OpenResty 状态信息", + "tags": [ + "OpenResty" + ], + "summary": "Load OpenResty status info", + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/response.NginxStatus" + } + } + } + } + }, + "/openresty/update": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "更新 OpenResty 配置信息", + "consumes": [ + "application/json" + ], + "tags": [ + "OpenResty" + ], + "summary": "Update OpenResty conf", + "parameters": [ + { + "description": "request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/request.NginxConfigUpdate" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + }, + "x-panel-log": { + "BeforeFunctions": [ + { + "db": "websites", + "input_column": "id", + "input_value": "websiteId", + "isList": false, + "output_column": "primary_domain", + "output_value": "domain" + } + ], + "bodyKeys": [ + "websiteId" + ], + "formatEN": "Update nginx conf [domain]", + "formatZH": "更新 nginx 配置 [domain]", + "paramKeys": [] + } + } + }, + "/process/stop": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "停止进程", + "tags": [ + "Process" + ], + "summary": "Stop Process", + "parameters": [ + { + "description": "request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/request.ProcessReq" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + }, + "x-panel-log": { + "BeforeFunctions": [], + "bodyKeys": [ + "PID" + ], + "formatEN": "结束进程 [PID]", + "formatZH": "结束进程 [PID]", + "paramKeys": [] + } + } + }, + "/runtimes": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "创建运行环境", + "consumes": [ + "application/json" + ], + "tags": [ + "Runtime" + ], + "summary": "Create runtime", + "parameters": [ + { + "description": "request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/request.RuntimeCreate" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + }, + "x-panel-log": { + "BeforeFunctions": [], + "bodyKeys": [ + "name" + ], + "formatEN": "Create runtime [name]", + "formatZH": "创建运行环境 [name]", + "paramKeys": [] + } + } + }, + "/runtimes/:id": { + "get": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "获取运行环境", + "consumes": [ + "application/json" + ], + "tags": [ + "Runtime" + ], + "summary": "Get runtime", + "parameters": [ + { + "type": "string", + "description": "request", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK" + } + } + } + }, + "/runtimes/del": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "删除运行环境", + "consumes": [ + "application/json" + ], + "tags": [ + "Website" + ], + "summary": "Delete runtime", + "parameters": [ + { + "description": "request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/request.RuntimeDelete" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + }, + "x-panel-log": { + "BeforeFunctions": [], + "bodyKeys": [ + "id" + ], + "formatEN": "Delete website [name]", + "formatZH": "删除网站 [name]", + "paramKeys": [] + } + } + }, + "/runtimes/node/modules": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "获取 Node 项目的 modules", + "consumes": [ + "application/json" + ], + "tags": [ + "Runtime" + ], + "summary": "Get Node modules", + "parameters": [ + { + "description": "request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/request.NodeModuleReq" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + } + } + }, + "/runtimes/node/modules/operate": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "操作 Node 项目 modules", + "consumes": [ + "application/json" + ], + "tags": [ + "Runtime" + ], + "summary": "Operate Node modules", + "parameters": [ + { + "description": "request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/request.NodeModuleReq" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + } + } + }, + "/runtimes/node/package": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "获取 Node 项目的 scripts", + "consumes": [ + "application/json" + ], + "tags": [ + "Runtime" + ], + "summary": "Get Node package scripts", + "parameters": [ + { + "description": "request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/request.NodePackageReq" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + } + } + }, + "/runtimes/operate": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "操作运行环境", + "consumes": [ + "application/json" + ], + "tags": [ + "Runtime" + ], + "summary": "Operate runtime", + "parameters": [ + { + "description": "request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/request.RuntimeOperate" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + }, + "x-panel-log": { + "BeforeFunctions": [], + "bodyKeys": [ + "id" + ], + "formatEN": "Operate runtime [name]", + "formatZH": "操作运行环境 [name]", + "paramKeys": [] + } + } + }, + "/runtimes/php/extensions": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "Create Extensions", + "consumes": [ + "application/json" + ], + "tags": [ + "PHP Extensions" + ], + "summary": "Create Extensions", + "parameters": [ + { + "description": "request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/request.PHPExtensionsCreate" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + } + } + }, + "/runtimes/php/extensions/del": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "Delete Extensions", + "consumes": [ + "application/json" + ], + "tags": [ + "PHP Extensions" + ], + "summary": "Delete Extensions", + "parameters": [ + { + "description": "request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/request.PHPExtensionsDelete" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + } + } + }, + "/runtimes/php/extensions/search": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "Page Extensions", + "consumes": [ + "application/json" + ], + "tags": [ + "PHP Extensions" + ], + "summary": "Page Extensions", + "parameters": [ + { + "description": "request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/request.PHPExtensionsSearch" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/response.PHPExtensionsDTO" + } + } + } + } + } + }, + "/runtimes/php/extensions/update": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "Update Extensions", + "consumes": [ + "application/json" + ], + "tags": [ + "PHP Extensions" + ], + "summary": "Update Extensions", + "parameters": [ + { + "description": "request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/request.PHPExtensionsUpdate" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + } + } + }, + "/runtimes/search": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "获取运行环境列表", + "consumes": [ + "application/json" + ], + "tags": [ + "Runtime" + ], + "summary": "List runtimes", + "parameters": [ + { + "description": "request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/request.RuntimeSearch" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + } + } + }, + "/runtimes/sync": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "同步运行环境状态", + "consumes": [ + "application/json" + ], + "tags": [ + "Runtime" + ], + "summary": "Sync runtime status", + "responses": { + "200": { + "description": "OK" + } + } + } + }, + "/runtimes/update": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "更新运行环境", + "consumes": [ + "application/json" + ], + "tags": [ + "Runtime" + ], + "summary": "Update runtime", + "parameters": [ + { + "description": "request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/request.RuntimeUpdate" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + }, + "x-panel-log": { + "BeforeFunctions": [], + "bodyKeys": [ + "name" + ], + "formatEN": "Update runtime [name]", + "formatZH": "更新运行环境 [name]", + "paramKeys": [] + } + } + }, + "/settings/backup": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "创建备份账号", + "consumes": [ + "application/json" + ], + "tags": [ + "Backup Account" + ], + "summary": "Create backup account", + "parameters": [ + { + "description": "request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.BackupOperate" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + }, + "x-panel-log": { + "BeforeFunctions": [], + "bodyKeys": [ + "type" + ], + "formatEN": "create backup account [type]", + "formatZH": "创建备份账号 [type]", + "paramKeys": [] + } + } + }, + "/settings/backup/backup": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "备份系统数据", + "consumes": [ + "application/json" + ], + "tags": [ + "Backup Account" + ], + "summary": "Backup system data", + "parameters": [ + { + "description": "request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.CommonBackup" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + }, + "x-panel-log": { + "BeforeFunctions": [], + "bodyKeys": [ + "type", + "name", + "detailName" + ], + "formatEN": "backup [type] data [name][detailName]", + "formatZH": "备份 [type] 数据 [name][detailName]", + "paramKeys": [] + } + } + }, + "/settings/backup/del": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "删除备份账号", + "consumes": [ + "application/json" + ], + "tags": [ + "Backup Account" + ], + "summary": "Delete backup account", + "parameters": [ + { + "description": "request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.OperateByID" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + }, + "x-panel-log": { + "BeforeFunctions": [ + { + "db": "backup_accounts", + "input_column": "id", + "input_value": "id", + "isList": false, + "output_column": "type", + "output_value": "types" + } + ], + "bodyKeys": [ + "id" + ], + "formatEN": "delete backup account [types]", + "formatZH": "删除备份账号 [types]", + "paramKeys": [] + } + } + }, + "/settings/backup/onedrive": { + "get": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "获取 OneDrive 信息", + "consumes": [ + "application/json" + ], + "tags": [ + "Backup Account" + ], + "summary": "Load OneDrive info", + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/dto.OneDriveInfo" + } + } + } + } + }, + "/settings/backup/record/del": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "删除备份记录", + "consumes": [ + "application/json" + ], + "tags": [ + "Backup Account" + ], + "summary": "Delete backup record", + "parameters": [ + { + "description": "request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.BatchDeleteReq" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + }, + "x-panel-log": { + "BeforeFunctions": [ + { + "db": "backup_records", + "input_column": "id", + "input_value": "ids", + "isList": true, + "output_column": "file_name", + "output_value": "files" + } + ], + "bodyKeys": [ + "ids" + ], + "formatEN": "delete backup records [files]", + "formatZH": "删除备份记录 [files]", + "paramKeys": [] + } + } + }, + "/settings/backup/record/download": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "下载备份记录", + "consumes": [ + "application/json" + ], + "tags": [ + "Backup Account" + ], + "summary": "Download backup record", + "parameters": [ + { + "description": "request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.DownloadRecord" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + }, + "x-panel-log": { + "BeforeFunctions": [], + "bodyKeys": [ + "source", + "fileName" + ], + "formatEN": "download backup records [source][fileName]", + "formatZH": "下载备份记录 [source][fileName]", + "paramKeys": [] + } + } + }, + "/settings/backup/record/search": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "获取备份记录列表分页", + "consumes": [ + "application/json" + ], + "tags": [ + "Backup Account" + ], + "summary": "Page backup records", + "parameters": [ + { + "description": "request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.RecordSearch" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + } + } + }, + "/settings/backup/record/search/bycronjob": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "通过计划任务获取备份记录列表分页", + "consumes": [ + "application/json" + ], + "tags": [ + "Backup Account" + ], + "summary": "Page backup records by cronjob", + "parameters": [ + { + "description": "request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.RecordSearchByCronjob" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + } + } + }, + "/settings/backup/recover": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "恢复系统数据", + "consumes": [ + "application/json" + ], + "tags": [ + "Backup Account" + ], + "summary": "Recover system data", + "parameters": [ + { + "description": "request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.CommonRecover" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + }, + "x-panel-log": { + "BeforeFunctions": [], + "bodyKeys": [ + "type", + "name", + "detailName", + "file" + ], + "formatEN": "recover [type] data [name][detailName] from [file]", + "formatZH": "从 [file] 恢复 [type] 数据 [name][detailName]", + "paramKeys": [] + } + } + }, + "/settings/backup/recover/byupload": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "从上传恢复系统数据", + "consumes": [ + "application/json" + ], + "tags": [ + "Backup Account" + ], + "summary": "Recover system data by upload", + "parameters": [ + { + "description": "request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.CommonRecover" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + }, + "x-panel-log": { + "BeforeFunctions": [], + "bodyKeys": [ + "type", + "name", + "detailName", + "file" + ], + "formatEN": "recover [type] data [name][detailName] from [file]", + "formatZH": "从 [file] 恢复 [type] 数据 [name][detailName]", + "paramKeys": [] + } + } + }, + "/settings/backup/refresh/onedrive": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "刷新 OneDrive token", + "tags": [ + "Backup Account" + ], + "summary": "Refresh OneDrive token", + "responses": { + "200": { + "description": "OK" + } + } + } + }, + "/settings/backup/search": { + "get": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "获取备份账号列表", + "tags": [ + "Backup Account" + ], + "summary": "List backup accounts", + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/dto.BackupInfo" + } + } + } + } + }, + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "获取 bucket 列表", + "consumes": [ + "application/json" + ], + "tags": [ + "Backup Account" + ], + "summary": "List buckets", + "parameters": [ + { + "description": "request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.ForBuckets" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "array", + "items": { + "type": "string" + } + } + } + } + } + }, + "/settings/backup/search/files": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "获取备份账号内文件列表", + "consumes": [ + "application/json" + ], + "tags": [ + "Backup Account" + ], + "summary": "List files from backup accounts", + "parameters": [ + { + "description": "request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.BackupSearchFile" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "array", + "items": { + "type": "string" + } + } + } + } + } + }, + "/settings/backup/update": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "更新备份账号信息", + "consumes": [ + "application/json" + ], + "tags": [ + "Backup Account" + ], + "summary": "Update backup account", + "parameters": [ + { + "description": "request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.BackupOperate" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + }, + "x-panel-log": { + "BeforeFunctions": [], + "bodyKeys": [ + "type" + ], + "formatEN": "update backup account [types]", + "formatZH": "更新备份账号 [types]", + "paramKeys": [] + } + } + }, + "/settings/basedir": { + "get": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "获取安装根目录", + "tags": [ + "System Setting" + ], + "summary": "Load local backup dir", + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "string" + } + } + } + } + }, + "/settings/bind/update": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "更新系统监听信息", + "consumes": [ + "application/json" + ], + "tags": [ + "System Setting" + ], + "summary": "Update system bind info", + "parameters": [ + { + "description": "request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.BindInfo" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + }, + "x-panel-log": { + "BeforeFunctions": [], + "bodyKeys": [ + "ipv6", + "bindAddress" + ], + "formatEN": "update system bind info =\u003e ipv6: [ipv6], 监听 IP: [bindAddress]", + "formatZH": "修改系统监听信息 =\u003e ipv6: [ipv6], 监听 IP: [bindAddress]", + "paramKeys": [] + } + } + }, + "/settings/expired/handle": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "重置过期系统登录密码", + "consumes": [ + "application/json" + ], + "tags": [ + "System Setting" + ], + "summary": "Reset system password expired", + "parameters": [ + { + "description": "request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.PasswordUpdate" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + }, + "x-panel-log": { + "BeforeFunctions": [], + "bodyKeys": [], + "formatEN": "reset an expired Password", + "formatZH": "重置过期密码", + "paramKeys": [] + } + } + }, + "/settings/interface": { + "get": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "获取系统地址信息", + "consumes": [ + "application/json" + ], + "tags": [ + "System Setting" + ], + "summary": "Load system address", + "responses": { + "200": { + "description": "OK" + } + } + } + }, + "/settings/menu/update": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "隐藏高级功能菜单", + "consumes": [ + "application/json" + ], + "tags": [ + "System Setting" + ], + "summary": "Update system setting", + "parameters": [ + { + "description": "request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.SettingUpdate" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + }, + "x-panel-log": { + "BeforeFunctions": [], + "bodyKeys": [], + "formatEN": "Hide advanced feature menu.", + "formatZH": "隐藏高级功能菜单", + "paramKeys": [] + } + } + }, + "/settings/mfa": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "获取 mfa 信息", + "consumes": [ + "application/json" + ], + "tags": [ + "System Setting" + ], + "summary": "Load mfa info", + "parameters": [ + { + "description": "request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.MfaCredential" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/mfa.Otp" + } + } + } + } + }, + "/settings/mfa/bind": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "Mfa 绑定", + "consumes": [ + "application/json" + ], + "tags": [ + "System Setting" + ], + "summary": "Bind mfa", + "parameters": [ + { + "description": "request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.MfaCredential" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + }, + "x-panel-log": { + "BeforeFunctions": [], + "bodyKeys": [], + "formatEN": "bind mfa", + "formatZH": "mfa 绑定", + "paramKeys": [] + } + } + }, + "/settings/password/update": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "更新系统登录密码", + "consumes": [ + "application/json" + ], + "tags": [ + "System Setting" + ], + "summary": "Update system password", + "parameters": [ + { + "description": "request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.PasswordUpdate" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + }, + "x-panel-log": { + "BeforeFunctions": [], + "bodyKeys": [], + "formatEN": "update system password", + "formatZH": "修改系统密码", + "paramKeys": [] + } + } + }, + "/settings/port/update": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "更新系统端口", + "consumes": [ + "application/json" + ], + "tags": [ + "System Setting" + ], + "summary": "Update system port", + "parameters": [ + { + "description": "request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.PortUpdate" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + }, + "x-panel-log": { + "BeforeFunctions": [], + "bodyKeys": [ + "serverPort" + ], + "formatEN": "update system port =\u003e [serverPort]", + "formatZH": "修改系统端口 =\u003e [serverPort]", + "paramKeys": [] + } + } + }, + "/settings/proxy/update": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "服务器代理配置", + "consumes": [ + "application/json" + ], + "tags": [ + "System Setting" + ], + "summary": "Update proxy setting", + "parameters": [ + { + "description": "request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.ProxyUpdate" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + }, + "x-panel-log": { + "BeforeFunctions": [], + "bodyKeys": [ + "proxyUrl", + "proxyPort" + ], + "formatEN": "set proxy [proxyPort]:[proxyPort].", + "formatZH": "服务器代理配置 [proxyPort]:[proxyPort]", + "paramKeys": [] + } + } + }, + "/settings/search": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "加载系统配置信息", + "tags": [ + "System Setting" + ], + "summary": "Load system setting info", + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/dto.SettingInfo" + } + } + } + } + }, + "/settings/search/available": { + "get": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "获取系统可用状态", + "tags": [ + "System Setting" + ], + "summary": "Load system available status", + "responses": { + "200": { + "description": "OK" + } + } + } + }, + "/settings/snapshot": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "创建系统快照", + "consumes": [ + "application/json" + ], + "tags": [ + "System Setting" + ], + "summary": "Create system snapshot", + "parameters": [ + { + "description": "request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.SnapshotCreate" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + }, + "x-panel-log": { + "BeforeFunctions": [], + "bodyKeys": [ + "from", + "description" + ], + "formatEN": "Create system backup [description] to [from]", + "formatZH": "创建系统快照 [description] 到 [from]", + "paramKeys": [] + } + } + }, + "/settings/snapshot/del": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "删除系统快照", + "consumes": [ + "application/json" + ], + "tags": [ + "System Setting" + ], + "summary": "Delete system backup", + "parameters": [ + { + "description": "request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.SnapshotBatchDelete" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + }, + "x-panel-log": { + "BeforeFunctions": [ + { + "db": "snapshots", + "input_column": "id", + "input_value": "ids", + "isList": true, + "output_column": "name", + "output_value": "name" + } + ], + "bodyKeys": [ + "ids" + ], + "formatEN": "Delete system backup [name]", + "formatZH": "删除系统快照 [name]", + "paramKeys": [] + } + } + }, + "/settings/snapshot/description/update": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "更新快照描述信息", + "consumes": [ + "application/json" + ], + "tags": [ + "System Setting" + ], + "summary": "Update snapshot description", + "parameters": [ + { + "description": "request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.UpdateDescription" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + }, + "x-panel-log": { + "BeforeFunctions": [ + { + "db": "snapshots", + "input_column": "id", + "input_value": "id", + "isList": false, + "output_column": "name", + "output_value": "name" + } + ], + "bodyKeys": [ + "id", + "description" + ], + "formatEN": "The description of the snapshot [name] is modified =\u003e [description]", + "formatZH": "快照 [name] 描述信息修改 [description]", + "paramKeys": [] + } + } + }, + "/settings/snapshot/import": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "导入已有快照", + "consumes": [ + "application/json" + ], + "tags": [ + "System Setting" + ], + "summary": "Import system snapshot", + "parameters": [ + { + "description": "request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.SnapshotImport" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + }, + "x-panel-log": { + "BeforeFunctions": [], + "bodyKeys": [ + "from", + "names" + ], + "formatEN": "Sync system snapshots [names] from [from]", + "formatZH": "从 [from] 同步系统快照 [names]", + "paramKeys": [] + } + } + }, + "/settings/snapshot/recover": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "从系统快照恢复", + "consumes": [ + "application/json" + ], + "tags": [ + "System Setting" + ], + "summary": "Recover system backup", + "parameters": [ + { + "description": "request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.SnapshotRecover" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + }, + "x-panel-log": { + "BeforeFunctions": [ + { + "db": "snapshots", + "input_column": "id", + "input_value": "id", + "isList": false, + "output_column": "name", + "output_value": "name" + } + ], + "bodyKeys": [ + "id" + ], + "formatEN": "Recover from system backup [name]", + "formatZH": "从系统快照 [name] 恢复", + "paramKeys": [] + } + } + }, + "/settings/snapshot/rollback": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "从系统快照回滚", + "consumes": [ + "application/json" + ], + "tags": [ + "System Setting" + ], + "summary": "Rollback system backup", + "parameters": [ + { + "description": "request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.SnapshotRecover" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + }, + "x-panel-log": { + "BeforeFunctions": [ + { + "db": "snapshots", + "input_column": "id", + "input_value": "id", + "isList": false, + "output_column": "name", + "output_value": "name" + } + ], + "bodyKeys": [ + "id" + ], + "formatEN": "Rollback from system backup [name]", + "formatZH": "从系统快照 [name] 回滚", + "paramKeys": [] + } + } + }, + "/settings/snapshot/search": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "获取系统快照列表分页", + "consumes": [ + "application/json" + ], + "tags": [ + "System Setting" + ], + "summary": "Page system snapshot", + "parameters": [ + { + "description": "request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.SearchWithPage" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/dto.PageResult" + } + } + } + } + }, + "/settings/snapshot/status": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "获取快照状态", + "consumes": [ + "application/json" + ], + "tags": [ + "System Setting" + ], + "summary": "Load Snapshot status", + "parameters": [ + { + "description": "request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.OperateByID" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + } + } + }, + "/settings/ssl/download": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "下载证书", + "tags": [ + "System Setting" + ], + "summary": "Download system cert", + "responses": { + "200": { + "description": "OK" + } + } + } + }, + "/settings/ssl/info": { + "get": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "获取证书信息", + "tags": [ + "System Setting" + ], + "summary": "Load system cert info", + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/dto.SettingInfo" + } + } + } + } + }, + "/settings/ssl/update": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "修改系统 ssl 登录", + "consumes": [ + "application/json" + ], + "tags": [ + "System Setting" + ], + "summary": "Update system ssl", + "parameters": [ + { + "description": "request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.SSLUpdate" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + }, + "x-panel-log": { + "BeforeFunctions": [], + "bodyKeys": [ + "ssl" + ], + "formatEN": "update system ssl =\u003e [ssl]", + "formatZH": "修改系统 ssl =\u003e [ssl]", + "paramKeys": [] + } + } + }, + "/settings/update": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "更新系统配置", + "consumes": [ + "application/json" + ], + "tags": [ + "System Setting" + ], + "summary": "Update system setting", + "parameters": [ + { + "description": "request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.SettingUpdate" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + }, + "x-panel-log": { + "BeforeFunctions": [], + "bodyKeys": [ + "key", + "value" + ], + "formatEN": "update system setting [key] =\u003e [value]", + "formatZH": "修改系统配置 [key] =\u003e [value]", + "paramKeys": [] + } + } + }, + "/settings/upgrade": { + "get": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "获取版本 release notes", + "consumes": [ + "application/json" + ], + "tags": [ + "System Setting" + ], + "summary": "Load release notes by version", + "parameters": [ + { + "description": "request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.Upgrade" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + } + }, + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "系统更新", + "consumes": [ + "application/json" + ], + "tags": [ + "System Setting" + ], + "summary": "Upgrade", + "parameters": [ + { + "description": "request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.Upgrade" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + }, + "x-panel-log": { + "BeforeFunctions": [], + "bodyKeys": [ + "version" + ], + "formatEN": "upgrade system =\u003e [version]", + "formatZH": "更新系统 =\u003e [version]", + "paramKeys": [] + } + } + }, + "/toolbox/clam": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "创建扫描规则", + "consumes": [ + "application/json" + ], + "tags": [ + "Clam" + ], + "summary": "Create clam", + "parameters": [ + { + "description": "request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.ClamCreate" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + }, + "x-panel-log": { + "BeforeFunctions": [], + "bodyKeys": [ + "name", + "path" + ], + "formatEN": "create clam [name][path]", + "formatZH": "创建扫描规则 [name][path]", + "paramKeys": [] + } + } + }, + "/toolbox/clam/base": { + "get": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "获取 Clam 基础信息", + "consumes": [ + "application/json" + ], + "tags": [ + "Clam" + ], + "summary": "Load clam base info", + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/dto.ClamBaseInfo" + } + } + } + } + }, + "/toolbox/clam/del": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "删除扫描规则", + "consumes": [ + "application/json" + ], + "tags": [ + "Clam" + ], + "summary": "Delete clam", + "parameters": [ + { + "description": "request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.ClamDelete" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + }, + "x-panel-log": { + "BeforeFunctions": [ + { + "db": "clams", + "input_column": "id", + "input_value": "ids", + "isList": true, + "output_column": "name", + "output_value": "names" + } + ], + "bodyKeys": [ + "ids" + ], + "formatEN": "delete clam [names]", + "formatZH": "删除扫描规则 [names]", + "paramKeys": [] + } + } + }, + "/toolbox/clam/file/search": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "获取扫描文件", + "consumes": [ + "application/json" + ], + "tags": [ + "Clam" + ], + "summary": "Load clam file", + "parameters": [ + { + "description": "request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.ClamFileReq" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/dto.PageResult" + } + } + } + } + }, + "/toolbox/clam/file/update": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "更新病毒扫描配置文件", + "consumes": [ + "application/json" + ], + "tags": [ + "Clam" + ], + "summary": "Update clam file", + "parameters": [ + { + "description": "request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.UpdateByNameAndFile" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + } + } + }, + "/toolbox/clam/handle": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "执行病毒扫描", + "consumes": [ + "application/json" + ], + "tags": [ + "Clam" + ], + "summary": "Handle clam scan", + "parameters": [ + { + "description": "request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.OperateByID" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + }, + "x-panel-log": { + "BeforeFunctions": [ + { + "db": "clams", + "input_column": "id", + "input_value": "id", + "isList": true, + "output_column": "name", + "output_value": "name" + } + ], + "bodyKeys": [ + "id" + ], + "formatEN": "handle clam scan [name]", + "formatZH": "执行病毒扫描 [name]", + "paramKeys": [] + } + } + }, + "/toolbox/clam/operate": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "修改 Clam 状态", + "consumes": [ + "application/json" + ], + "tags": [ + "Clam" + ], + "summary": "Operate Clam", + "parameters": [ + { + "description": "request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.Operate" + } + } + ], + "responses": {}, + "x-panel-log": { + "BeforeFunctions": [], + "bodyKeys": [ + "operation" + ], + "formatEN": "[operation] FTP", + "formatZH": "[operation] Clam", + "paramKeys": [] + } + } + }, + "/toolbox/clam/record/clean": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "清空扫描报告", + "consumes": [ + "application/json" + ], + "tags": [ + "Clam" + ], + "summary": "Clean clam record", + "parameters": [ + { + "description": "request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.OperateByID" + } + } + ], + "responses": {}, + "x-panel-log": { + "BeforeFunctions": [ + { + "db": "clams", + "input_column": "id", + "input_value": "id", + "isList": true, + "output_column": "name", + "output_value": "name" + } + ], + "bodyKeys": [ + "id" + ], + "formatEN": "clean clam record [name]", + "formatZH": "清空扫描报告 [name]", + "paramKeys": [] + } + } + }, + "/toolbox/clam/record/log": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "获取扫描结果详情", + "consumes": [ + "application/json" + ], + "tags": [ + "Clam" + ], + "summary": "Load clam record detail", + "parameters": [ + { + "description": "request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.ClamLogReq" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + } + } + }, + "/toolbox/clam/record/search": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "获取扫描结果列表分页", + "consumes": [ + "application/json" + ], + "tags": [ + "Clam" + ], + "summary": "Page clam record", + "parameters": [ + { + "description": "request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.ClamLogSearch" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/dto.PageResult" + } + } + } + } + }, + "/toolbox/clam/search": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "获取扫描规则列表分页", + "consumes": [ + "application/json" + ], + "tags": [ + "Clam" + ], + "summary": "Page clam", + "parameters": [ + { + "description": "request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.SearchWithPage" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/dto.PageResult" + } + } + } + } + }, + "/toolbox/clam/status/update": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "修改扫描规则状态", + "consumes": [ + "application/json" + ], + "tags": [ + "Clam" + ], + "summary": "Update clam status", + "parameters": [ + { + "description": "request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.ClamUpdateStatus" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + }, + "x-panel-log": { + "BeforeFunctions": [ + { + "db": "clams", + "input_column": "id", + "input_value": "id", + "isList": false, + "output_column": "name", + "output_value": "name" + } + ], + "bodyKeys": [ + "id", + "status" + ], + "formatEN": "change the status of clam [name] to [status].", + "formatZH": "修改扫描规则 [name] 状态为 [status]", + "paramKeys": [] + } + } + }, + "/toolbox/clam/update": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "修改扫描规则", + "consumes": [ + "application/json" + ], + "tags": [ + "Clam" + ], + "summary": "Update clam", + "parameters": [ + { + "description": "request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.ClamUpdate" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + }, + "x-panel-log": { + "BeforeFunctions": [], + "bodyKeys": [ + "name", + "path" + ], + "formatEN": "update clam [name][path]", + "formatZH": "修改扫描规则 [name][path]", + "paramKeys": [] + } + } + }, + "/toolbox/clean": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "清理系统垃圾文件", + "consumes": [ + "application/json" + ], + "tags": [ + "Device" + ], + "summary": "Clean system", + "parameters": [ + { + "description": "request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/dto.Clean" + } + } + } + ], + "responses": { + "200": { + "description": "OK" + } + }, + "x-panel-log": { + "BeforeFunctions": [], + "bodyKeys": [], + "formatEN": "Clean system junk files", + "formatZH": "清理系统垃圾文件", + "paramKeys": [] + } + } + }, + "/toolbox/device/base": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "获取设备基础信息", + "tags": [ + "Device" + ], + "summary": "Load device base info", + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/dto.DeviceBaseInfo" + } + } + } + } + }, + "/toolbox/device/check/dns": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "检查系统 DNS 配置可用性", + "consumes": [ + "application/json" + ], + "tags": [ + "Device" + ], + "summary": "Check device DNS conf", + "parameters": [ + { + "description": "request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.SettingUpdate" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + } + } + }, + "/toolbox/device/conf": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "获取系统配置文件", + "consumes": [ + "application/json" + ], + "tags": [ + "Device" + ], + "summary": "load conf", + "parameters": [ + { + "description": "request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.OperationWithName" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + } + } + }, + "/toolbox/device/update/byconf": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "通过文件修改配置", + "consumes": [ + "application/json" + ], + "tags": [ + "Device" + ], + "summary": "Update device conf by file", + "parameters": [ + { + "description": "request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.UpdateByNameAndFile" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + } + } + }, + "/toolbox/device/update/conf": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "修改系统参数", + "consumes": [ + "application/json" + ], + "tags": [ + "Device" + ], + "summary": "Update device", + "parameters": [ + { + "description": "request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.SettingUpdate" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + }, + "x-panel-log": { + "BeforeFunctions": [], + "bodyKeys": [ + "key", + "value" + ], + "formatEN": "update device conf [key] =\u003e [value]", + "formatZH": "修改主机参数 [key] =\u003e [value]", + "paramKeys": [] + } + } + }, + "/toolbox/device/update/host": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "修改系统 hosts", + "tags": [ + "Device" + ], + "summary": "Update device hosts", + "responses": { + "200": { + "description": "OK" + } + }, + "x-panel-log": { + "BeforeFunctions": [], + "bodyKeys": [ + "key", + "value" + ], + "formatEN": "update device host [key] =\u003e [value]", + "formatZH": "修改主机 Host [key] =\u003e [value]", + "paramKeys": [] + } + } + }, + "/toolbox/device/update/passwd": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "修改系统密码", + "consumes": [ + "application/json" + ], + "tags": [ + "Device" + ], + "summary": "Update device passwd", + "parameters": [ + { + "description": "request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.ChangePasswd" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + } + } + }, + "/toolbox/device/update/swap": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "修改系统 Swap", + "consumes": [ + "application/json" + ], + "tags": [ + "Device" + ], + "summary": "Update device swap", + "parameters": [ + { + "description": "request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.SwapHelper" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + }, + "x-panel-log": { + "BeforeFunctions": [], + "bodyKeys": [ + "operate", + "path" + ], + "formatEN": "[operate] device swap [path]", + "formatZH": "[operate] 主机 swap [path]", + "paramKeys": [] + } + } + }, + "/toolbox/device/zone/options": { + "get": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "获取系统可用时区选项", + "consumes": [ + "application/json" + ], + "tags": [ + "Device" + ], + "summary": "list time zone options", + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "Array" + } + } + } + } + }, + "/toolbox/fail2ban/base": { + "get": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "获取 Fail2ban 基础信息", + "tags": [ + "Fail2ban" + ], + "summary": "Load fail2ban base info", + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/dto.Fail2BanBaseInfo" + } + } + } + } + }, + "/toolbox/fail2ban/load/conf": { + "get": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "获取 fail2ban 配置文件", + "consumes": [ + "application/json" + ], + "tags": [ + "Fail2ban" + ], + "summary": "Load fail2ban conf", + "responses": { + "200": { + "description": "OK" + } + } + } + }, + "/toolbox/fail2ban/operate": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "修改 Fail2ban 状态", + "consumes": [ + "application/json" + ], + "tags": [ + "Fail2ban" + ], + "summary": "Operate fail2ban", + "parameters": [ + { + "description": "request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.Operate" + } + } + ], + "responses": {}, + "x-panel-log": { + "BeforeFunctions": [], + "bodyKeys": [ + "operation" + ], + "formatEN": "[operation] Fail2ban", + "formatZH": "[operation] Fail2ban", + "paramKeys": [] + } + } + }, + "/toolbox/fail2ban/operate/sshd": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "配置 sshd", + "consumes": [ + "application/json" + ], + "tags": [ + "Fail2ban" + ], + "summary": "Operate sshd of fail2ban", + "parameters": [ + { + "description": "request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.Operate" + } + } + ], + "responses": {} + } + }, + "/toolbox/fail2ban/search": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "获取 Fail2ban ip", + "consumes": [ + "application/json" + ], + "tags": [ + "Fail2ban" + ], + "summary": "Page fail2ban ip list", + "parameters": [ + { + "description": "request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.Fail2BanSearch" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "Array" + } + } + } + } + }, + "/toolbox/fail2ban/update": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "修改 Fail2ban 配置", + "consumes": [ + "application/json" + ], + "tags": [ + "Fail2ban" + ], + "summary": "Update fail2ban conf", + "parameters": [ + { + "description": "request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.Fail2BanUpdate" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + }, + "x-panel-log": { + "BeforeFunctions": [], + "bodyKeys": [ + "key", + "value" + ], + "formatEN": "update fail2ban conf [key] =\u003e [value]", + "formatZH": "修改 Fail2ban 配置 [key] =\u003e [value]", + "paramKeys": [] + } + } + }, + "/toolbox/fail2ban/update/byconf": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "通过文件修改 fail2ban 配置", + "consumes": [ + "application/json" + ], + "tags": [ + "Fail2ban" + ], + "summary": "Update fail2ban conf by file", + "parameters": [ + { + "description": "request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.UpdateByFile" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + } + } + }, + "/toolbox/ftp": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "创建 FTP 账户", + "consumes": [ + "application/json" + ], + "tags": [ + "FTP" + ], + "summary": "Create FTP user", + "parameters": [ + { + "description": "request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.FtpCreate" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + }, + "x-panel-log": { + "BeforeFunctions": [], + "bodyKeys": [ + "user", + "path" + ], + "formatEN": "create FTP [user][path]", + "formatZH": "创建 FTP 账户 [user][path]", + "paramKeys": [] + } + } + }, + "/toolbox/ftp/base": { + "get": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "获取 FTP 基础信息", + "tags": [ + "FTP" + ], + "summary": "Load FTP base info", + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/dto.FtpBaseInfo" + } + } + } + } + }, + "/toolbox/ftp/del": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "删除 FTP 账户", + "consumes": [ + "application/json" + ], + "tags": [ + "FTP" + ], + "summary": "Delete FTP user", + "parameters": [ + { + "description": "request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.BatchDeleteReq" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + }, + "x-panel-log": { + "BeforeFunctions": [ + { + "db": "ftps", + "input_column": "id", + "input_value": "ids", + "isList": true, + "output_column": "user", + "output_value": "users" + } + ], + "bodyKeys": [ + "ids" + ], + "formatEN": "delete FTP users [users]", + "formatZH": "删除 FTP 账户 [users]", + "paramKeys": [] + } + } + }, + "/toolbox/ftp/log/search": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "获取 FTP 操作日志", + "consumes": [ + "application/json" + ], + "tags": [ + "FTP" + ], + "summary": "Load FTP operation log", + "parameters": [ + { + "description": "request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.FtpLogSearch" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/dto.PageResult" + } + } + } + } + }, + "/toolbox/ftp/operate": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "修改 FTP 状态", + "consumes": [ + "application/json" + ], + "tags": [ + "FTP" + ], + "summary": "Operate FTP", + "parameters": [ + { + "description": "request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.Operate" + } + } + ], + "responses": {}, + "x-panel-log": { + "BeforeFunctions": [], + "bodyKeys": [ + "operation" + ], + "formatEN": "[operation] FTP", + "formatZH": "[operation] FTP", + "paramKeys": [] + } + } + }, + "/toolbox/ftp/search": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "获取 FTP 账户列表分页", + "consumes": [ + "application/json" + ], + "tags": [ + "FTP" + ], + "summary": "Page FTP user", + "parameters": [ + { + "description": "request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.SearchWithPage" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/dto.PageResult" + } + } + } + } + }, + "/toolbox/ftp/sync": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "同步 FTP 账户", + "consumes": [ + "application/json" + ], + "tags": [ + "FTP" + ], + "summary": "Sync FTP user", + "parameters": [ + { + "description": "request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.BatchDeleteReq" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + }, + "x-panel-log": { + "BeforeFunctions": [], + "bodyKeys": [], + "formatEN": "sync FTP users", + "formatZH": "同步 FTP 账户", + "paramKeys": [] + } + } + }, + "/toolbox/ftp/update": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "修改 FTP 账户", + "consumes": [ + "application/json" + ], + "tags": [ + "FTP" + ], + "summary": "Update FTP user", + "parameters": [ + { + "description": "request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.FtpUpdate" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + }, + "x-panel-log": { + "BeforeFunctions": [], + "bodyKeys": [ + "user", + "path" + ], + "formatEN": "update FTP [user][path]", + "formatZH": "修改 FTP 账户 [user][path]", + "paramKeys": [] + } + } + }, + "/toolbox/scan": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "扫描系统垃圾文件", + "tags": [ + "Device" + ], + "summary": "Scan system", + "responses": { + "200": { + "description": "OK" + } + }, + "x-panel-log": { + "BeforeFunctions": [], + "bodyKeys": [], + "formatEN": "scan System Junk Files", + "formatZH": "扫描系统垃圾文件", + "paramKeys": [] + } + } + }, + "/websites": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "创建网站", + "consumes": [ + "application/json" + ], + "tags": [ + "Website" + ], + "summary": "Create website", + "parameters": [ + { + "description": "request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/request.WebsiteCreate" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + }, + "x-panel-log": { + "BeforeFunctions": [], + "bodyKeys": [ + "primaryDomain" + ], + "formatEN": "Create website [primaryDomain]", + "formatZH": "创建网站 [primaryDomain]", + "paramKeys": [] + } + } + }, + "/websites/:id": { + "get": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "通过 id 查询网站", + "consumes": [ + "application/json" + ], + "tags": [ + "Website" + ], + "summary": "Search website by id", + "parameters": [ + { + "type": "integer", + "description": "request", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/response.WebsiteDTO" + } + } + } + } + }, + "/websites/:id/config/:type": { + "get": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "通过 id 查询网站 nginx", + "consumes": [ + "application/json" + ], + "tags": [ + "Website Nginx" + ], + "summary": "Search website nginx by id", + "parameters": [ + { + "type": "integer", + "description": "request", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/response.FileInfo" + } + } + } + } + }, + "/websites/:id/https": { + "get": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "获取 https 配置", + "consumes": [ + "application/json" + ], + "tags": [ + "Website HTTPS" + ], + "summary": "Load https conf", + "parameters": [ + { + "type": "integer", + "description": "request", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/response.WebsiteHTTPS" + } + } + } + }, + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "更新 https 配置", + "consumes": [ + "application/json" + ], + "tags": [ + "Website HTTPS" + ], + "summary": "Update https conf", + "parameters": [ + { + "description": "request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/request.WebsiteHTTPSOp" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/response.WebsiteHTTPS" + } + } + }, + "x-panel-log": { + "BeforeFunctions": [ + { + "db": "websites", + "input_column": "id", + "input_value": "websiteId", + "isList": false, + "output_column": "primary_domain", + "output_value": "domain" + } + ], + "bodyKeys": [ + "websiteId" + ], + "formatEN": "Update website https [domain] conf", + "formatZH": "更新网站 [domain] https 配置", + "paramKeys": [] + } + } + }, + "/websites/acme": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "创建网站 acme", + "consumes": [ + "application/json" + ], + "tags": [ + "Website Acme" + ], + "summary": "Create website acme account", + "parameters": [ + { + "description": "request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/request.WebsiteAcmeAccountCreate" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/response.WebsiteAcmeAccountDTO" + } + } + }, + "x-panel-log": { + "BeforeFunctions": [], + "bodyKeys": [ + "email" + ], + "formatEN": "Create website acme [email]", + "formatZH": "创建网站 acme [email]", + "paramKeys": [] + } + } + }, + "/websites/acme/del": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "删除网站 acme", + "consumes": [ + "application/json" + ], + "tags": [ + "Website Acme" + ], + "summary": "Delete website acme account", + "parameters": [ + { + "description": "request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/request.WebsiteResourceReq" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + }, + "x-panel-log": { + "BeforeFunctions": [ + { + "db": "website_acme_accounts", + "input_column": "id", + "input_value": "id", + "isList": false, + "output_column": "email", + "output_value": "email" + } + ], + "bodyKeys": [ + "id" + ], + "formatEN": "Delete website acme [email]", + "formatZH": "删除网站 acme [email]", + "paramKeys": [] + } + } + }, + "/websites/acme/search": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "获取网站 acme 列表分页", + "consumes": [ + "application/json" + ], + "tags": [ + "Website Acme" + ], + "summary": "Page website acme accounts", + "parameters": [ + { + "description": "request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.PageInfo" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/dto.PageResult" + } + } + } + } + }, + "/websites/auths": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "获取密码访问配置", + "consumes": [ + "application/json" + ], + "tags": [ + "Website" + ], + "summary": "Get AuthBasic conf", + "parameters": [ + { + "description": "request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/request.NginxAuthReq" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + } + } + }, + "/websites/auths/update": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "更新密码访问配置", + "consumes": [ + "application/json" + ], + "tags": [ + "Website" + ], + "summary": "Get AuthBasic conf", + "parameters": [ + { + "description": "request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/request.NginxAuthUpdate" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + } + } + }, + "/websites/ca": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "创建网站 ca", + "consumes": [ + "application/json" + ], + "tags": [ + "Website CA" + ], + "summary": "Create website ca", + "parameters": [ + { + "description": "request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/request.WebsiteCACreate" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/request.WebsiteCACreate" + } + } + }, + "x-panel-log": { + "BeforeFunctions": [], + "bodyKeys": [ + "name" + ], + "formatEN": "Create website ca [name]", + "formatZH": "创建网站 ca [name]", + "paramKeys": [] + } + } + }, + "/websites/ca/del": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "删除网站 ca", + "consumes": [ + "application/json" + ], + "tags": [ + "Website CA" + ], + "summary": "Delete website ca", + "parameters": [ + { + "description": "request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/request.WebsiteCommonReq" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + }, + "x-panel-log": { + "BeforeFunctions": [ + { + "db": "website_cas", + "input_column": "id", + "input_value": "id", + "isList": false, + "output_column": "name", + "output_value": "name" + } + ], + "bodyKeys": [ + "id" + ], + "formatEN": "Delete website ca [name]", + "formatZH": "删除网站 ca [name]", + "paramKeys": [] + } + } + }, + "/websites/ca/download": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "下载 CA 证书文件", + "consumes": [ + "application/json" + ], + "tags": [ + "Website CA" + ], + "summary": "Download CA file", + "parameters": [ + { + "description": "request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/request.WebsiteResourceReq" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + }, + "x-panel-log": { + "BeforeFunctions": [ + { + "db": "website_cas", + "input_column": "id", + "input_value": "id", + "isList": false, + "output_column": "name", + "output_value": "name" + } + ], + "bodyKeys": [ + "id" + ], + "formatEN": "download ca file [name]", + "formatZH": "下载 CA 证书文件 [name]", + "paramKeys": [] + } + } + }, + "/websites/ca/obtain": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "自签 SSL 证书", + "consumes": [ + "application/json" + ], + "tags": [ + "Website CA" + ], + "summary": "Obtain SSL", + "parameters": [ + { + "description": "request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/request.WebsiteCAObtain" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + }, + "x-panel-log": { + "BeforeFunctions": [ + { + "db": "website_cas", + "input_column": "id", + "input_value": "id", + "isList": false, + "output_column": "name", + "output_value": "name" + } + ], + "bodyKeys": [ + "id" + ], + "formatEN": "Obtain SSL [name]", + "formatZH": "自签 SSL 证书 [name]", + "paramKeys": [] + } + } + }, + "/websites/ca/renew": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "续签 SSL 证书", + "consumes": [ + "application/json" + ], + "tags": [ + "Website CA" + ], + "summary": "Obtain SSL", + "parameters": [ + { + "description": "request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/request.WebsiteCAObtain" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + }, + "x-panel-log": { + "BeforeFunctions": [ + { + "db": "website_cas", + "input_column": "id", + "input_value": "id", + "isList": false, + "output_column": "name", + "output_value": "name" + } + ], + "bodyKeys": [ + "id" + ], + "formatEN": "Obtain SSL [name]", + "formatZH": "自签 SSL 证书 [name]", + "paramKeys": [] + } + } + }, + "/websites/ca/search": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "获取网站 ca 列表分页", + "consumes": [ + "application/json" + ], + "tags": [ + "Website CA" + ], + "summary": "Page website ca", + "parameters": [ + { + "description": "request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/request.WebsiteCASearch" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/dto.PageResult" + } + } + } + } + }, + "/websites/ca/{id}": { + "get": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "获取网站 ca", + "consumes": [ + "application/json" + ], + "tags": [ + "Website CA" + ], + "summary": "Get website ca", + "parameters": [ + { + "type": "integer", + "description": "id", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/response.WebsiteCADTO" + } + } + } + } + }, + "/websites/check": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "网站创建前检查", + "consumes": [ + "application/json" + ], + "tags": [ + "Website" + ], + "summary": "Check before create website", + "parameters": [ + { + "description": "request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/request.WebsiteInstallCheckReq" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/response.WebsitePreInstallCheck" + } + } + } + } + } + }, + "/websites/config": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "获取 nginx 配置", + "consumes": [ + "application/json" + ], + "tags": [ + "Website Nginx" + ], + "summary": "Load nginx conf", + "parameters": [ + { + "description": "request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/request.NginxScopeReq" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/response.WebsiteNginxConfig" + } + } + } + } + }, + "/websites/config/update": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "更新 nginx 配置", + "consumes": [ + "application/json" + ], + "tags": [ + "Website Nginx" + ], + "summary": "Update nginx conf", + "parameters": [ + { + "description": "request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/request.NginxConfigUpdate" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + }, + "x-panel-log": { + "BeforeFunctions": [ + { + "db": "websites", + "input_column": "id", + "input_value": "websiteId", + "isList": false, + "output_column": "primary_domain", + "output_value": "domain" + } + ], + "bodyKeys": [ + "websiteId" + ], + "formatEN": "Nginx conf update [domain]", + "formatZH": "nginx 配置修改 [domain]", + "paramKeys": [] + } + } + }, + "/websites/default/html/:type": { + "get": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "获取默认 html", + "consumes": [ + "application/json" + ], + "tags": [ + "Website" + ], + "summary": "Get default html", + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/response.FileInfo" + } + } + } + } + }, + "/websites/default/html/update": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "更新默认 html", + "consumes": [ + "application/json" + ], + "tags": [ + "Website" + ], + "summary": "Update default html", + "parameters": [ + { + "description": "request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/request.WebsiteHtmlUpdate" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + }, + "x-panel-log": { + "BeforeFunctions": [], + "bodyKeys": [ + "type" + ], + "formatEN": "Update default html", + "formatZH": "更新默认 html", + "paramKeys": [] + } + } + }, + "/websites/default/server": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "操作网站日志", + "consumes": [ + "application/json" + ], + "tags": [ + "Website" + ], + "summary": "Change default server", + "parameters": [ + { + "description": "request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/request.WebsiteDefaultUpdate" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + }, + "x-panel-log": { + "BeforeFunctions": [ + { + "db": "websites", + "input_column": "id", + "input_value": "id", + "isList": false, + "output_column": "primary_domain", + "output_value": "domain" + } + ], + "bodyKeys": [ + "id", + "operate" + ], + "formatEN": "Change default server =\u003e [domain]", + "formatZH": "修改默认 server =\u003e [domain]", + "paramKeys": [] + } + } + }, + "/websites/del": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "删除网站", + "consumes": [ + "application/json" + ], + "tags": [ + "Website" + ], + "summary": "Delete website", + "parameters": [ + { + "description": "request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/request.WebsiteDelete" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + }, + "x-panel-log": { + "BeforeFunctions": [ + { + "db": "websites", + "input_column": "id", + "input_value": "id", + "isList": false, + "output_column": "primary_domain", + "output_value": "domain" + } + ], + "bodyKeys": [ + "id" + ], + "formatEN": "Delete website [domain]", + "formatZH": "删除网站 [domain]", + "paramKeys": [] + } + } + }, + "/websites/dir": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "获取网站目录配置", + "consumes": [ + "application/json" + ], + "tags": [ + "Website" + ], + "summary": "Get website dir", + "parameters": [ + { + "description": "request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/request.WebsiteCommonReq" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + } + } + }, + "/websites/dir/permission": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "更新网站目录权限", + "consumes": [ + "application/json" + ], + "tags": [ + "Website" + ], + "summary": "Update Site Dir permission", + "parameters": [ + { + "description": "request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/request.WebsiteUpdateDirPermission" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + }, + "x-panel-log": { + "BeforeFunctions": [ + { + "db": "websites", + "input_column": "id", + "input_value": "id", + "isList": false, + "output_column": "primary_domain", + "output_value": "domain" + } + ], + "bodyKeys": [ + "id" + ], + "formatEN": "Update domain [domain] dir permission", + "formatZH": "更新网站 [domain] 目录权限", + "paramKeys": [] + } + } + }, + "/websites/dir/update": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "更新网站目录", + "consumes": [ + "application/json" + ], + "tags": [ + "Website" + ], + "summary": "Update Site Dir", + "parameters": [ + { + "description": "request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/request.WebsiteUpdateDir" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + }, + "x-panel-log": { + "BeforeFunctions": [ + { + "db": "websites", + "input_column": "id", + "input_value": "id", + "isList": false, + "output_column": "primary_domain", + "output_value": "domain" + } + ], + "bodyKeys": [ + "id" + ], + "formatEN": "Update domain [domain] dir", + "formatZH": "更新网站 [domain] 目录", + "paramKeys": [] + } + } + }, + "/websites/dns": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "创建网站 dns", + "consumes": [ + "application/json" + ], + "tags": [ + "Website DNS" + ], + "summary": "Create website dns account", + "parameters": [ + { + "description": "request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/request.WebsiteDnsAccountCreate" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + }, + "x-panel-log": { + "BeforeFunctions": [], + "bodyKeys": [ + "name" + ], + "formatEN": "Create website dns [name]", + "formatZH": "创建网站 dns [name]", + "paramKeys": [] + } + } + }, + "/websites/dns/del": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "删除网站 dns", + "consumes": [ + "application/json" + ], + "tags": [ + "Website DNS" + ], + "summary": "Delete website dns account", + "parameters": [ + { + "description": "request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/request.WebsiteResourceReq" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + }, + "x-panel-log": { + "BeforeFunctions": [ + { + "db": "website_dns_accounts", + "input_column": "id", + "input_value": "id", + "isList": false, + "output_column": "name", + "output_value": "name" + } + ], + "bodyKeys": [ + "id" + ], + "formatEN": "Delete website dns [name]", + "formatZH": "删除网站 dns [name]", + "paramKeys": [] + } + } + }, + "/websites/dns/search": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "获取网站 dns 列表分页", + "consumes": [ + "application/json" + ], + "tags": [ + "Website DNS" + ], + "summary": "Page website dns accounts", + "parameters": [ + { + "description": "request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.PageInfo" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/dto.PageResult" + } + } + } + } + }, + "/websites/dns/update": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "更新网站 dns", + "consumes": [ + "application/json" + ], + "tags": [ + "Website DNS" + ], + "summary": "Update website dns account", + "parameters": [ + { + "description": "request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/request.WebsiteDnsAccountUpdate" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + }, + "x-panel-log": { + "BeforeFunctions": [], + "bodyKeys": [ + "name" + ], + "formatEN": "Update website dns [name]", + "formatZH": "更新网站 dns [name]", + "paramKeys": [] + } + } + }, + "/websites/domains": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "创建网站域名", + "consumes": [ + "application/json" + ], + "tags": [ + "Website Domain" + ], + "summary": "Create website domain", + "parameters": [ + { + "description": "request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/request.WebsiteDomainCreate" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/model.WebsiteDomain" + } + } + }, + "x-panel-log": { + "BeforeFunctions": [], + "bodyKeys": [ + "domain" + ], + "formatEN": "Create domain [domain]", + "formatZH": "创建域名 [domain]", + "paramKeys": [] + } + } + }, + "/websites/domains/:websiteId": { + "get": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "通过网站 id 查询域名", + "consumes": [ + "application/json" + ], + "tags": [ + "Website Domain" + ], + "summary": "Search website domains by websiteId", + "parameters": [ + { + "type": "integer", + "description": "request", + "name": "websiteId", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/model.WebsiteDomain" + } + } + } + } + } + }, + "/websites/domains/del": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "删除网站域名", + "consumes": [ + "application/json" + ], + "tags": [ + "Website Domain" + ], + "summary": "Delete website domain", + "parameters": [ + { + "description": "request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/request.WebsiteDomainDelete" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + }, + "x-panel-log": { + "BeforeFunctions": [ + { + "db": "website_domains", + "input_column": "id", + "input_value": "id", + "isList": false, + "output_column": "domain", + "output_value": "domain" + } + ], + "bodyKeys": [ + "id" + ], + "formatEN": "Delete domain [domain]", + "formatZH": "删除域名 [domain]", + "paramKeys": [] + } + } + }, + "/websites/leech": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "获取防盗链配置", + "consumes": [ + "application/json" + ], + "tags": [ + "Website" + ], + "summary": "Get AntiLeech conf", + "parameters": [ + { + "description": "request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/request.NginxCommonReq" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + } + } + }, + "/websites/leech/update": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "更新防盗链配置", + "consumes": [ + "application/json" + ], + "tags": [ + "Website" + ], + "summary": "Update AntiLeech", + "parameters": [ + { + "description": "request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/request.NginxAntiLeechUpdate" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + } + } + }, + "/websites/list": { + "get": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "获取网站列表", + "tags": [ + "Website" + ], + "summary": "List websites", + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/response.WebsiteDTO" + } + } + } + } + } + }, + "/websites/log": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "操作网站日志", + "consumes": [ + "application/json" + ], + "tags": [ + "Website" + ], + "summary": "Operate website log", + "parameters": [ + { + "description": "request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/request.WebsiteLogReq" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/response.WebsiteLog" + } + } + }, + "x-panel-log": { + "BeforeFunctions": [ + { + "db": "websites", + "input_column": "id", + "input_value": "id", + "isList": false, + "output_column": "primary_domain", + "output_value": "domain" + } + ], + "bodyKeys": [ + "id", + "operate" + ], + "formatEN": "[domain][operate] logs", + "formatZH": "[domain][operate] 日志", + "paramKeys": [] + } + } + }, + "/websites/nginx/update": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "更新 网站 nginx 配置", + "consumes": [ + "application/json" + ], + "tags": [ + "Website Nginx" + ], + "summary": "Update website nginx conf", + "parameters": [ + { + "description": "request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/request.WebsiteNginxUpdate" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + }, + "x-panel-log": { + "BeforeFunctions": [ + { + "db": "websites", + "input_column": "id", + "input_value": "id", + "isList": false, + "output_column": "primary_domain", + "output_value": "domain" + } + ], + "bodyKeys": [ + "id" + ], + "formatEN": "[domain] Nginx conf update", + "formatZH": "[domain] Nginx 配置修改", + "paramKeys": [] + } + } + }, + "/websites/operate": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "操作网站", + "consumes": [ + "application/json" + ], + "tags": [ + "Website" + ], + "summary": "Operate website", + "parameters": [ + { + "description": "request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/request.WebsiteOp" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + }, + "x-panel-log": { + "BeforeFunctions": [ + { + "db": "websites", + "input_column": "id", + "input_value": "id", + "isList": false, + "output_column": "primary_domain", + "output_value": "domain" + } + ], + "bodyKeys": [ + "id", + "operate" + ], + "formatEN": "[operate] website [domain]", + "formatZH": "[operate] 网站 [domain]", + "paramKeys": [] + } + } + }, + "/websites/options": { + "get": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "获取网站列表", + "tags": [ + "Website" + ], + "summary": "List website names", + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "array", + "items": { + "type": "string" + } + } + } + } + } + }, + "/websites/php/config": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "更新 网站 PHP 配置", + "consumes": [ + "application/json" + ], + "tags": [ + "Website PHP" + ], + "summary": "Update website php conf", + "parameters": [ + { + "description": "request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/request.WebsitePHPConfigUpdate" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + }, + "x-panel-log": { + "BeforeFunctions": [ + { + "db": "websites", + "input_column": "id", + "input_value": "id", + "isList": false, + "output_column": "primary_domain", + "output_value": "domain" + } + ], + "bodyKeys": [ + "id" + ], + "formatEN": "[domain] PHP conf update", + "formatZH": "[domain] PHP 配置修改", + "paramKeys": [] + } + } + }, + "/websites/php/config/:id": { + "get": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "获取网站 php 配置", + "consumes": [ + "application/json" + ], + "tags": [ + "Website" + ], + "summary": "Load website php conf", + "parameters": [ + { + "type": "integer", + "description": "request", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/response.PHPConfig" + } + } + } + } + }, + "/websites/php/update": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "更新 php 配置文件", + "consumes": [ + "application/json" + ], + "tags": [ + "Website PHP" + ], + "summary": "Update php conf", + "parameters": [ + { + "description": "request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/request.WebsitePHPFileUpdate" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + }, + "x-panel-log": { + "BeforeFunctions": [ + { + "db": "websites", + "input_column": "id", + "input_value": "websiteId", + "isList": false, + "output_column": "primary_domain", + "output_value": "domain" + } + ], + "bodyKeys": [ + "websiteId" + ], + "formatEN": "Nginx conf update [domain]", + "formatZH": "php 配置修改 [domain]", + "paramKeys": [] + } + } + }, + "/websites/php/version": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "变更 php 版本", + "consumes": [ + "application/json" + ], + "tags": [ + "Website PHP" + ], + "summary": "Update php version", + "parameters": [ + { + "description": "request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/request.WebsitePHPVersionReq" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + }, + "x-panel-log": { + "BeforeFunctions": [ + { + "db": "websites", + "input_column": "id", + "input_value": "websiteId", + "isList": false, + "output_column": "primary_domain", + "output_value": "domain" + } + ], + "bodyKeys": [ + "websiteId" + ], + "formatEN": "php version update [domain]", + "formatZH": "php 版本变更 [domain]", + "paramKeys": [] + } + } + }, + "/websites/proxies": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "获取反向代理配置", + "consumes": [ + "application/json" + ], + "tags": [ + "Website" + ], + "summary": "Get proxy conf", + "parameters": [ + { + "description": "request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/request.WebsiteProxyReq" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + } + } + }, + "/websites/proxies/update": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "修改反向代理配置", + "consumes": [ + "application/json" + ], + "tags": [ + "Website" + ], + "summary": "Update proxy conf", + "parameters": [ + { + "description": "request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/request.WebsiteProxyConfig" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + }, + "x-panel-log": { + "BeforeFunctions": [ + { + "db": "websites", + "input_column": "id", + "input_value": "id", + "isList": false, + "output_column": "primary_domain", + "output_value": "domain" + } + ], + "bodyKeys": [ + "id" + ], + "formatEN": "Update domain [domain] proxy config", + "formatZH": "修改网站 [domain] 反向代理配置 ", + "paramKeys": [] + } + } + }, + "/websites/proxy/file": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "更新反向代理文件", + "consumes": [ + "application/json" + ], + "tags": [ + "Website" + ], + "summary": "Update proxy file", + "parameters": [ + { + "description": "request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/request.NginxProxyUpdate" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + }, + "x-panel-log": { + "BeforeFunctions": [ + { + "db": "websites", + "input_column": "id", + "input_value": "websiteID", + "isList": false, + "output_column": "primary_domain", + "output_value": "domain" + } + ], + "bodyKeys": [ + "websiteID" + ], + "formatEN": "Nginx conf proxy file update [domain]", + "formatZH": "更新反向代理文件 [domain]", + "paramKeys": [] + } + } + }, + "/websites/redirect": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "获取重定向配置", + "consumes": [ + "application/json" + ], + "tags": [ + "Website" + ], + "summary": "Get redirect conf", + "parameters": [ + { + "description": "request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/request.WebsiteProxyReq" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + } + } + }, + "/websites/redirect/file": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "更新重定向文件", + "consumes": [ + "application/json" + ], + "tags": [ + "Website" + ], + "summary": "Update redirect file", + "parameters": [ + { + "description": "request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/request.NginxRedirectUpdate" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + }, + "x-panel-log": { + "BeforeFunctions": [ + { + "db": "websites", + "input_column": "id", + "input_value": "websiteID", + "isList": false, + "output_column": "primary_domain", + "output_value": "domain" + } + ], + "bodyKeys": [ + "websiteID" + ], + "formatEN": "Nginx conf redirect file update [domain]", + "formatZH": "更新重定向文件 [domain]", + "paramKeys": [] + } + } + }, + "/websites/redirect/update": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "修改重定向配置", + "consumes": [ + "application/json" + ], + "tags": [ + "Website" + ], + "summary": "Update redirect conf", + "parameters": [ + { + "description": "request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/request.NginxRedirectReq" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + }, + "x-panel-log": { + "BeforeFunctions": [ + { + "db": "websites", + "input_column": "id", + "input_value": "websiteID", + "isList": false, + "output_column": "primary_domain", + "output_value": "domain" + } + ], + "bodyKeys": [ + "websiteID" + ], + "formatEN": "Update domain [domain] redirect config", + "formatZH": "修改网站 [domain] 重定向理配置 ", + "paramKeys": [] + } + } + }, + "/websites/rewrite": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "获取伪静态配置", + "consumes": [ + "application/json" + ], + "tags": [ + "Website" + ], + "summary": "Get rewrite conf", + "parameters": [ + { + "description": "request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/request.NginxRewriteReq" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + } + } + }, + "/websites/rewrite/update": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "更新伪静态配置", + "consumes": [ + "application/json" + ], + "tags": [ + "Website" + ], + "summary": "Update rewrite conf", + "parameters": [ + { + "description": "request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/request.NginxRewriteUpdate" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + }, + "x-panel-log": { + "BeforeFunctions": [ + { + "db": "websites", + "input_column": "id", + "input_value": "websiteID", + "isList": false, + "output_column": "primary_domain", + "output_value": "domain" + } + ], + "bodyKeys": [ + "websiteID" + ], + "formatEN": "Nginx conf rewrite update [domain]", + "formatZH": "伪静态配置修改 [domain]", + "paramKeys": [] + } + } + }, + "/websites/search": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "获取网站列表分页", + "consumes": [ + "application/json" + ], + "tags": [ + "Website" + ], + "summary": "Page websites", + "parameters": [ + { + "description": "request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/request.WebsiteSearch" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/dto.PageResult" + } + } + } + } + }, + "/websites/ssl": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "创建网站 ssl", + "consumes": [ + "application/json" + ], + "tags": [ + "Website SSL" + ], + "summary": "Create website ssl", + "parameters": [ + { + "description": "request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/request.WebsiteSSLCreate" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/request.WebsiteSSLCreate" + } + } + }, + "x-panel-log": { + "BeforeFunctions": [], + "bodyKeys": [ + "primaryDomain" + ], + "formatEN": "Create website ssl [primaryDomain]", + "formatZH": "创建网站 ssl [primaryDomain]", + "paramKeys": [] + } + } + }, + "/websites/ssl/:id": { + "get": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "通过 id 查询 ssl", + "consumes": [ + "application/json" + ], + "tags": [ + "Website SSL" + ], + "summary": "Search website ssl by id", + "parameters": [ + { + "type": "integer", + "description": "request", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK" + } + } + } + }, + "/websites/ssl/del": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "删除网站 ssl", + "consumes": [ + "application/json" + ], + "tags": [ + "Website SSL" + ], + "summary": "Delete website ssl", + "parameters": [ + { + "description": "request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/request.WebsiteBatchDelReq" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + }, + "x-panel-log": { + "BeforeFunctions": [ + { + "db": "website_ssls", + "input_column": "id", + "input_value": "ids", + "isList": true, + "output_column": "primary_domain", + "output_value": "domain" + } + ], + "bodyKeys": [ + "ids" + ], + "formatEN": "Delete ssl [domain]", + "formatZH": "删除 ssl [domain]", + "paramKeys": [] + } + } + }, + "/websites/ssl/download": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "下载证书文件", + "consumes": [ + "application/json" + ], + "tags": [ + "Website SSL" + ], + "summary": "Download SSL file", + "parameters": [ + { + "description": "request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/request.WebsiteResourceReq" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + }, + "x-panel-log": { + "BeforeFunctions": [ + { + "db": "website_ssls", + "input_column": "id", + "input_value": "id", + "isList": false, + "output_column": "primary_domain", + "output_value": "domain" + } + ], + "bodyKeys": [ + "id" + ], + "formatEN": "download ssl file [domain]", + "formatZH": "下载证书文件 [domain]", + "paramKeys": [] + } + } + }, + "/websites/ssl/obtain": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "申请证书", + "consumes": [ + "application/json" + ], + "tags": [ + "Website SSL" + ], + "summary": "Apply ssl", + "parameters": [ + { + "description": "request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/request.WebsiteSSLApply" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + }, + "x-panel-log": { + "BeforeFunctions": [ + { + "db": "website_ssls", + "input_column": "id", + "input_value": "ID", + "isList": false, + "output_column": "primary_domain", + "output_value": "domain" + } + ], + "bodyKeys": [ + "ID" + ], + "formatEN": "apply ssl [domain]", + "formatZH": "申请证书 [domain]", + "paramKeys": [] + } + } + }, + "/websites/ssl/resolve": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "解析网站 ssl", + "consumes": [ + "application/json" + ], + "tags": [ + "Website SSL" + ], + "summary": "Resolve website ssl", + "parameters": [ + { + "description": "request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/request.WebsiteDNSReq" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/response.WebsiteDNSRes" + } + } + } + } + } + }, + "/websites/ssl/search": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "获取网站 ssl 列表分页", + "consumes": [ + "application/json" + ], + "tags": [ + "Website SSL" + ], + "summary": "Page website ssl", + "parameters": [ + { + "description": "request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/request.WebsiteSSLSearch" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + } + } + }, + "/websites/ssl/update": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "更新 ssl", + "consumes": [ + "application/json" + ], + "tags": [ + "Website SSL" + ], + "summary": "Update ssl", + "parameters": [ + { + "description": "request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/request.WebsiteSSLUpdate" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + }, + "x-panel-log": { + "BeforeFunctions": [ + { + "db": "website_ssls", + "input_column": "id", + "input_value": "id", + "isList": false, + "output_column": "primary_domain", + "output_value": "domain" + } + ], + "bodyKeys": [ + "id" + ], + "formatEN": "Update ssl config [domain]", + "formatZH": "更新证书设置 [domain]", + "paramKeys": [] + } + } + }, + "/websites/ssl/upload": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "上传 ssl", + "consumes": [ + "application/json" + ], + "tags": [ + "Website SSL" + ], + "summary": "Upload ssl", + "parameters": [ + { + "description": "request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/request.WebsiteSSLUpload" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + }, + "x-panel-log": { + "BeforeFunctions": [], + "bodyKeys": [ + "type" + ], + "formatEN": "Upload ssl [type]", + "formatZH": "上传 ssl [type]", + "paramKeys": [] + } + } + }, + "/websites/ssl/website/:websiteId": { + "get": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "通过网站 id 查询 ssl", + "consumes": [ + "application/json" + ], + "tags": [ + "Website SSL" + ], + "summary": "Search website ssl by website id", + "parameters": [ + { + "type": "integer", + "description": "request", + "name": "websiteId", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK" + } + } + } + }, + "/websites/update": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "更新网站", + "consumes": [ + "application/json" + ], + "tags": [ + "Website" + ], + "summary": "Update website", + "parameters": [ + { + "description": "request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/request.WebsiteUpdate" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + }, + "x-panel-log": { + "BeforeFunctions": [], + "bodyKeys": [ + "primaryDomain" + ], + "formatEN": "Update website [primaryDomain]", + "formatZH": "更新网站 [primaryDomain]", + "paramKeys": [] + } + } + } + }, + "definitions": { + "dto.AddrRuleOperate": { + "type": "object", + "required": [ + "address", + "operation", + "strategy" + ], + "properties": { + "address": { + "type": "string" + }, + "description": { + "type": "string" + }, + "operation": { + "type": "string", + "enum": [ + "add", + "remove" + ] + }, + "strategy": { + "type": "string", + "enum": [ + "accept", + "drop" + ] + } + } + }, + "dto.AddrRuleUpdate": { + "type": "object", + "properties": { + "newRule": { + "$ref": "#/definitions/dto.AddrRuleOperate" + }, + "oldRule": { + "$ref": "#/definitions/dto.AddrRuleOperate" + } + } + }, + "dto.AppInstallInfo": { + "type": "object", + "properties": { + "id": { + "type": "integer" + }, + "key": { + "type": "string" + }, + "name": { + "type": "string" + } + } + }, + "dto.AppResource": { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "type": { + "type": "string" + } + } + }, + "dto.AppVersion": { + "type": "object", + "properties": { + "detailId": { + "type": "integer" + }, + "dockerCompose": { + "type": "string" + }, + "version": { + "type": "string" + } + } + }, + "dto.BackupInfo": { + "type": "object", + "properties": { + "backupPath": { + "type": "string" + }, + "bucket": { + "type": "string" + }, + "createdAt": { + "type": "string" + }, + "id": { + "type": "integer" + }, + "type": { + "type": "string" + }, + "vars": { + "type": "string" + } + } + }, + "dto.BackupOperate": { + "type": "object", + "required": [ + "type", + "vars" + ], + "properties": { + "accessKey": { + "type": "string" + }, + "backupPath": { + "type": "string" + }, + "bucket": { + "type": "string" + }, + "credential": { + "type": "string" + }, + "id": { + "type": "integer" + }, + "type": { + "type": "string" + }, + "vars": { + "type": "string" + } + } + }, + "dto.BackupSearchFile": { + "type": "object", + "required": [ + "type" + ], + "properties": { + "type": { + "type": "string" + } + } + }, + "dto.BatchDelete": { + "type": "object", + "required": [ + "names" + ], + "properties": { + "force": { + "type": "boolean" + }, + "names": { + "type": "array", + "items": { + "type": "string" + } + } + } + }, + "dto.BatchDeleteReq": { + "type": "object", + "required": [ + "ids" + ], + "properties": { + "ids": { + "type": "array", + "items": { + "type": "integer" + } + } + } + }, + "dto.BatchRuleOperate": { + "type": "object", + "required": [ + "type" + ], + "properties": { + "rules": { + "type": "array", + "items": { + "$ref": "#/definitions/dto.PortRuleOperate" + } + }, + "type": { + "type": "string" + } + } + }, + "dto.BindInfo": { + "type": "object", + "required": [ + "bindAddress", + "ipv6" + ], + "properties": { + "bindAddress": { + "type": "string" + }, + "ipv6": { + "type": "string", + "enum": [ + "enable", + "disable" + ] + } + } + }, + "dto.BindUser": { + "type": "object", + "required": [ + "database", + "db", + "password", + "permission", + "username" + ], + "properties": { + "database": { + "type": "string" + }, + "db": { + "type": "string" + }, + "password": { + "type": "string" + }, + "permission": { + "type": "string" + }, + "username": { + "type": "string" + } + } + }, + "dto.CaptchaResponse": { + "type": "object", + "properties": { + "captchaID": { + "type": "string" + }, + "imagePath": { + "type": "string" + } + } + }, + "dto.ChangeDBInfo": { + "type": "object", + "required": [ + "database", + "from", + "type", + "value" + ], + "properties": { + "database": { + "type": "string" + }, + "from": { + "type": "string", + "enum": [ + "local", + "remote" + ] + }, + "id": { + "type": "integer" + }, + "type": { + "type": "string", + "enum": [ + "mysql", + "mariadb", + "postgresql" + ] + }, + "value": { + "type": "string" + } + } + }, + "dto.ChangeHostGroup": { + "type": "object", + "required": [ + "groupID", + "id" + ], + "properties": { + "groupID": { + "type": "integer" + }, + "id": { + "type": "integer" + } + } + }, + "dto.ChangePasswd": { + "type": "object", + "properties": { + "passwd": { + "type": "string" + }, + "user": { + "type": "string" + } + } + }, + "dto.ChangeRedisPass": { + "type": "object", + "required": [ + "database" + ], + "properties": { + "database": { + "type": "string" + }, + "value": { + "type": "string" + } + } + }, + "dto.ClamBaseInfo": { + "type": "object", + "properties": { + "freshIsActive": { + "type": "boolean" + }, + "freshIsExist": { + "type": "boolean" + }, + "freshVersion": { + "type": "string" + }, + "isActive": { + "type": "boolean" + }, + "isExist": { + "type": "boolean" + }, + "version": { + "type": "string" + } + } + }, + "dto.ClamCreate": { + "type": "object", + "properties": { + "description": { + "type": "string" + }, + "infectedDir": { + "type": "string" + }, + "infectedStrategy": { + "type": "string" + }, + "name": { + "type": "string" + }, + "path": { + "type": "string" + }, + "spec": { + "type": "string" + }, + "status": { + "type": "string" + } + } + }, + "dto.ClamDelete": { + "type": "object", + "required": [ + "ids" + ], + "properties": { + "ids": { + "type": "array", + "items": { + "type": "integer" + } + }, + "removeInfected": { + "type": "boolean" + }, + "removeRecord": { + "type": "boolean" + } + } + }, + "dto.ClamFileReq": { + "type": "object", + "required": [ + "name" + ], + "properties": { + "name": { + "type": "string" + }, + "tail": { + "type": "string" + } + } + }, + "dto.ClamLogReq": { + "type": "object", + "properties": { + "clamName": { + "type": "string" + }, + "recordName": { + "type": "string" + }, + "tail": { + "type": "string" + } + } + }, + "dto.ClamLogSearch": { + "type": "object", + "required": [ + "page", + "pageSize" + ], + "properties": { + "clamID": { + "type": "integer" + }, + "endTime": { + "type": "string" + }, + "page": { + "type": "integer" + }, + "pageSize": { + "type": "integer" + }, + "startTime": { + "type": "string" + } + } + }, + "dto.ClamUpdate": { + "type": "object", + "properties": { + "description": { + "type": "string" + }, + "id": { + "type": "integer" + }, + "infectedDir": { + "type": "string" + }, + "infectedStrategy": { + "type": "string" + }, + "name": { + "type": "string" + }, + "path": { + "type": "string" + }, + "spec": { + "type": "string" + } + } + }, + "dto.ClamUpdateStatus": { + "type": "object", + "properties": { + "id": { + "type": "integer" + }, + "status": { + "type": "string" + } + } + }, + "dto.Clean": { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "size": { + "type": "integer" + }, + "treeType": { + "type": "string" + } + } + }, + "dto.CleanLog": { + "type": "object", + "required": [ + "logType" + ], + "properties": { + "logType": { + "type": "string", + "enum": [ + "login", + "operation" + ] + } + } + }, + "dto.CommandInfo": { + "type": "object", + "properties": { + "command": { + "type": "string" + }, + "groupBelong": { + "type": "string" + }, + "groupID": { + "type": "integer" + }, + "id": { + "type": "integer" + }, + "name": { + "type": "string" + } + } + }, + "dto.CommandOperate": { + "type": "object", + "required": [ + "command", + "name" + ], + "properties": { + "command": { + "type": "string" + }, + "groupBelong": { + "type": "string" + }, + "groupID": { + "type": "integer" + }, + "id": { + "type": "integer" + }, + "name": { + "type": "string" + } + } + }, + "dto.CommonBackup": { + "type": "object", + "required": [ + "type" + ], + "properties": { + "detailName": { + "type": "string" + }, + "name": { + "type": "string" + }, + "secret": { + "type": "string" + }, + "type": { + "type": "string", + "enum": [ + "app", + "mysql", + "mariadb", + "redis", + "website", + "postgresql" + ] + } + } + }, + "dto.CommonRecover": { + "type": "object", + "required": [ + "source", + "type" + ], + "properties": { + "detailName": { + "type": "string" + }, + "file": { + "type": "string" + }, + "name": { + "type": "string" + }, + "secret": { + "type": "string" + }, + "source": { + "type": "string", + "enum": [ + "OSS", + "S3", + "SFTP", + "MINIO", + "LOCAL", + "COS", + "KODO", + "OneDrive", + "WebDAV" + ] + }, + "type": { + "type": "string", + "enum": [ + "app", + "mysql", + "mariadb", + "redis", + "website", + "postgresql" + ] + } + } + }, + "dto.ComposeCreate": { + "type": "object", + "required": [ + "from" + ], + "properties": { + "file": { + "type": "string" + }, + "from": { + "type": "string", + "enum": [ + "edit", + "path", + "template" + ] + }, + "name": { + "type": "string" + }, + "path": { + "type": "string" + }, + "template": { + "type": "integer" + } + } + }, + "dto.ComposeOperation": { + "type": "object", + "required": [ + "name", + "operation", + "path" + ], + "properties": { + "name": { + "type": "string" + }, + "operation": { + "type": "string", + "enum": [ + "start", + "stop", + "down" + ] + }, + "path": { + "type": "string" + }, + "withFile": { + "type": "boolean" + } + } + }, + "dto.ComposeTemplateCreate": { + "type": "object", + "required": [ + "name" + ], + "properties": { + "content": { + "type": "string" + }, + "description": { + "type": "string" + }, + "name": { + "type": "string" + } + } + }, + "dto.ComposeTemplateInfo": { + "type": "object", + "properties": { + "content": { + "type": "string" + }, + "createdAt": { + "type": "string" + }, + "description": { + "type": "string" + }, + "id": { + "type": "integer" + }, + "name": { + "type": "string" + } + } + }, + "dto.ComposeTemplateUpdate": { + "type": "object", + "properties": { + "content": { + "type": "string" + }, + "description": { + "type": "string" + }, + "id": { + "type": "integer" + } + } + }, + "dto.ComposeUpdate": { + "type": "object", + "required": [ + "content", + "name", + "path" + ], + "properties": { + "content": { + "type": "string" + }, + "name": { + "type": "string" + }, + "path": { + "type": "string" + } + } + }, + "dto.ContainerCommit": { + "type": "object", + "required": [ + "containerID" + ], + "properties": { + "author": { + "type": "string" + }, + "comment": { + "type": "string" + }, + "containerID": { + "type": "string" + }, + "containerName": { + "type": "string" + }, + "newImageName": { + "type": "string" + }, + "pause": { + "type": "boolean" + } + } + }, + "dto.ContainerListStats": { + "type": "object", + "properties": { + "containerID": { + "type": "string" + }, + "cpuPercent": { + "type": "number" + }, + "cpuTotalUsage": { + "type": "integer" + }, + "memoryCache": { + "type": "integer" + }, + "memoryLimit": { + "type": "integer" + }, + "memoryPercent": { + "type": "number" + }, + "memoryUsage": { + "type": "integer" + }, + "percpuUsage": { + "type": "integer" + }, + "systemUsage": { + "type": "integer" + } + } + }, + "dto.ContainerOperate": { + "type": "object", + "required": [ + "image", + "name" + ], + "properties": { + "autoRemove": { + "type": "boolean" + }, + "cmd": { + "type": "array", + "items": { + "type": "string" + } + }, + "containerID": { + "type": "string" + }, + "cpuShares": { + "type": "integer" + }, + "entrypoint": { + "type": "array", + "items": { + "type": "string" + } + }, + "env": { + "type": "array", + "items": { + "type": "string" + } + }, + "exposedPorts": { + "type": "array", + "items": { + "$ref": "#/definitions/dto.PortHelper" + } + }, + "forcePull": { + "type": "boolean" + }, + "image": { + "type": "string" + }, + "ipv4": { + "type": "string" + }, + "ipv6": { + "type": "string" + }, + "labels": { + "type": "array", + "items": { + "type": "string" + } + }, + "memory": { + "type": "number" + }, + "name": { + "type": "string" + }, + "nanoCPUs": { + "type": "number" + }, + "network": { + "type": "string" + }, + "openStdin": { + "type": "boolean" + }, + "privileged": { + "type": "boolean" + }, + "publishAllPorts": { + "type": "boolean" + }, + "restartPolicy": { + "type": "string" + }, + "tty": { + "type": "boolean" + }, + "volumes": { + "type": "array", + "items": { + "$ref": "#/definitions/dto.VolumeHelper" + } + } + } + }, + "dto.ContainerOperation": { + "type": "object", + "required": [ + "names", + "operation" + ], + "properties": { + "names": { + "type": "array", + "items": { + "type": "string" + } + }, + "operation": { + "type": "string", + "enum": [ + "start", + "stop", + "restart", + "kill", + "pause", + "unpause", + "remove" + ] + } + } + }, + "dto.ContainerPrune": { + "type": "object", + "required": [ + "pruneType" + ], + "properties": { + "pruneType": { + "type": "string", + "enum": [ + "container", + "image", + "volume", + "network", + "buildcache" + ] + }, + "withTagAll": { + "type": "boolean" + } + } + }, + "dto.ContainerPruneReport": { + "type": "object", + "properties": { + "deletedNumber": { + "type": "integer" + }, + "spaceReclaimed": { + "type": "integer" + } + } + }, + "dto.ContainerRename": { + "type": "object", + "required": [ + "name", + "newName" + ], + "properties": { + "name": { + "type": "string" + }, + "newName": { + "type": "string" + } + } + }, + "dto.ContainerStats": { + "type": "object", + "properties": { + "cache": { + "type": "number" + }, + "cpuPercent": { + "type": "number" + }, + "ioRead": { + "type": "number" + }, + "ioWrite": { + "type": "number" + }, + "memory": { + "type": "number" + }, + "networkRX": { + "type": "number" + }, + "networkTX": { + "type": "number" + }, + "shotTime": { + "type": "string" + } + } + }, + "dto.ContainerUpgrade": { + "type": "object", + "required": [ + "image", + "name" + ], + "properties": { + "forcePull": { + "type": "boolean" + }, + "image": { + "type": "string" + }, + "name": { + "type": "string" + } + } + }, + "dto.CronjobBatchDelete": { + "type": "object", + "required": [ + "ids" + ], + "properties": { + "cleanData": { + "type": "boolean" + }, + "ids": { + "type": "array", + "items": { + "type": "integer" + } + } + } + }, + "dto.CronjobClean": { + "type": "object", + "required": [ + "cronjobID" + ], + "properties": { + "cleanData": { + "type": "boolean" + }, + "cronjobID": { + "type": "integer" + }, + "isDelete": { + "type": "boolean" + } + } + }, + "dto.CronjobCreate": { + "type": "object", + "required": [ + "name", + "spec", + "type" + ], + "properties": { + "appID": { + "type": "string" + }, + "backupAccounts": { + "type": "string" + }, + "command": { + "type": "string" + }, + "containerName": { + "type": "string" + }, + "dbName": { + "type": "string" + }, + "dbType": { + "type": "string" + }, + "defaultDownload": { + "type": "string" + }, + "exclusionRules": { + "type": "string" + }, + "name": { + "type": "string" + }, + "retainCopies": { + "type": "integer", + "minimum": 1 + }, + "script": { + "type": "string" + }, + "secret": { + "type": "string" + }, + "sourceDir": { + "type": "string" + }, + "spec": { + "type": "string" + }, + "type": { + "type": "string" + }, + "url": { + "type": "string" + }, + "website": { + "type": "string" + } + } + }, + "dto.CronjobDownload": { + "type": "object", + "required": [ + "backupAccountID", + "recordID" + ], + "properties": { + "backupAccountID": { + "type": "integer" + }, + "recordID": { + "type": "integer" + } + } + }, + "dto.CronjobUpdate": { + "type": "object", + "required": [ + "id", + "name", + "spec" + ], + "properties": { + "appID": { + "type": "string" + }, + "backupAccounts": { + "type": "string" + }, + "command": { + "type": "string" + }, + "containerName": { + "type": "string" + }, + "dbName": { + "type": "string" + }, + "dbType": { + "type": "string" + }, + "defaultDownload": { + "type": "string" + }, + "exclusionRules": { + "type": "string" + }, + "id": { + "type": "integer" + }, + "name": { + "type": "string" + }, + "retainCopies": { + "type": "integer", + "minimum": 1 + }, + "script": { + "type": "string" + }, + "secret": { + "type": "string" + }, + "sourceDir": { + "type": "string" + }, + "spec": { + "type": "string" + }, + "url": { + "type": "string" + }, + "website": { + "type": "string" + } + } + }, + "dto.CronjobUpdateStatus": { + "type": "object", + "required": [ + "id", + "status" + ], + "properties": { + "id": { + "type": "integer" + }, + "status": { + "type": "string" + } + } + }, + "dto.DBBaseInfo": { + "type": "object", + "properties": { + "containerName": { + "type": "string" + }, + "name": { + "type": "string" + }, + "port": { + "type": "integer" + } + } + }, + "dto.DBConfUpdateByFile": { + "type": "object", + "required": [ + "database", + "type" + ], + "properties": { + "database": { + "type": "string" + }, + "file": { + "type": "string" + }, + "type": { + "type": "string", + "enum": [ + "mysql", + "mariadb", + "postgresql", + "redis" + ] + } + } + }, + "dto.DaemonJsonConf": { + "type": "object", + "properties": { + "cgroupDriver": { + "type": "string" + }, + "experimental": { + "type": "boolean" + }, + "fixedCidrV6": { + "type": "string" + }, + "insecureRegistries": { + "type": "array", + "items": { + "type": "string" + } + }, + "ip6Tables": { + "type": "boolean" + }, + "iptables": { + "type": "boolean" + }, + "ipv6": { + "type": "boolean" + }, + "isSwarm": { + "type": "boolean" + }, + "liveRestore": { + "type": "boolean" + }, + "logMaxFile": { + "type": "string" + }, + "logMaxSize": { + "type": "string" + }, + "registryMirrors": { + "type": "array", + "items": { + "type": "string" + } + }, + "status": { + "type": "string" + }, + "version": { + "type": "string" + } + } + }, + "dto.DaemonJsonUpdateByFile": { + "type": "object", + "properties": { + "file": { + "type": "string" + } + } + }, + "dto.DashboardBase": { + "type": "object", + "properties": { + "appInstalledNumber": { + "type": "integer" + }, + "cpuCores": { + "type": "integer" + }, + "cpuLogicalCores": { + "type": "integer" + }, + "cpuModelName": { + "type": "string" + }, + "cronjobNumber": { + "type": "integer" + }, + "currentInfo": { + "$ref": "#/definitions/dto.DashboardCurrent" + }, + "databaseNumber": { + "type": "integer" + }, + "hostname": { + "type": "string" + }, + "kernelArch": { + "type": "string" + }, + "kernelVersion": { + "type": "string" + }, + "os": { + "type": "string" + }, + "platform": { + "type": "string" + }, + "platformFamily": { + "type": "string" + }, + "platformVersion": { + "type": "string" + }, + "virtualizationSystem": { + "type": "string" + }, + "websiteNumber": { + "type": "integer" + } + } + }, + "dto.DashboardCurrent": { + "type": "object", + "properties": { + "cpuPercent": { + "type": "array", + "items": { + "type": "number" + } + }, + "cpuTotal": { + "type": "integer" + }, + "cpuUsed": { + "type": "number" + }, + "cpuUsedPercent": { + "type": "number" + }, + "diskData": { + "type": "array", + "items": { + "$ref": "#/definitions/dto.DiskInfo" + } + }, + "gpuData": { + "type": "array", + "items": { + "$ref": "#/definitions/dto.GPUInfo" + } + }, + "ioCount": { + "type": "integer" + }, + "ioReadBytes": { + "type": "integer" + }, + "ioReadTime": { + "type": "integer" + }, + "ioWriteBytes": { + "type": "integer" + }, + "ioWriteTime": { + "type": "integer" + }, + "load1": { + "type": "number" + }, + "load15": { + "type": "number" + }, + "load5": { + "type": "number" + }, + "loadUsagePercent": { + "type": "number" + }, + "memoryAvailable": { + "type": "integer" + }, + "memoryTotal": { + "type": "integer" + }, + "memoryUsed": { + "type": "integer" + }, + "memoryUsedPercent": { + "type": "number" + }, + "netBytesRecv": { + "type": "integer" + }, + "netBytesSent": { + "type": "integer" + }, + "procs": { + "type": "integer" + }, + "shotTime": { + "type": "string" + }, + "swapMemoryAvailable": { + "type": "integer" + }, + "swapMemoryTotal": { + "type": "integer" + }, + "swapMemoryUsed": { + "type": "integer" + }, + "swapMemoryUsedPercent": { + "type": "number" + }, + "timeSinceUptime": { + "type": "string" + }, + "uptime": { + "type": "integer" + } + } + }, + "dto.DatabaseCreate": { + "type": "object", + "required": [ + "from", + "name", + "type", + "username", + "version" + ], + "properties": { + "address": { + "type": "string" + }, + "clientCert": { + "type": "string" + }, + "clientKey": { + "type": "string" + }, + "description": { + "type": "string" + }, + "from": { + "type": "string", + "enum": [ + "local", + "remote" + ] + }, + "name": { + "type": "string", + "maxLength": 256 + }, + "password": { + "type": "string" + }, + "port": { + "type": "integer" + }, + "rootCert": { + "type": "string" + }, + "skipVerify": { + "type": "boolean" + }, + "ssl": { + "type": "boolean" + }, + "type": { + "type": "string" + }, + "username": { + "type": "string" + }, + "version": { + "type": "string" + } + } + }, + "dto.DatabaseDelete": { + "type": "object", + "required": [ + "id" + ], + "properties": { + "deleteBackup": { + "type": "boolean" + }, + "forceDelete": { + "type": "boolean" + }, + "id": { + "type": "integer" + } + } + }, + "dto.DatabaseInfo": { + "type": "object", + "properties": { + "address": { + "type": "string" + }, + "clientCert": { + "type": "string" + }, + "clientKey": { + "type": "string" + }, + "createdAt": { + "type": "string" + }, + "description": { + "type": "string" + }, + "from": { + "type": "string" + }, + "id": { + "type": "integer" + }, + "name": { + "type": "string", + "maxLength": 256 + }, + "password": { + "type": "string" + }, + "port": { + "type": "integer" + }, + "rootCert": { + "type": "string" + }, + "skipVerify": { + "type": "boolean" + }, + "ssl": { + "type": "boolean" + }, + "type": { + "type": "string" + }, + "username": { + "type": "string" + }, + "version": { + "type": "string" + } + } + }, + "dto.DatabaseItem": { + "type": "object", + "properties": { + "database": { + "type": "string" + }, + "from": { + "type": "string" + }, + "id": { + "type": "integer" + }, + "name": { + "type": "string" + } + } + }, + "dto.DatabaseOption": { + "type": "object", + "properties": { + "address": { + "type": "string" + }, + "database": { + "type": "string" + }, + "from": { + "type": "string" + }, + "id": { + "type": "integer" + }, + "type": { + "type": "string" + }, + "version": { + "type": "string" + } + } + }, + "dto.DatabaseSearch": { + "type": "object", + "required": [ + "order", + "orderBy", + "page", + "pageSize" + ], + "properties": { + "info": { + "type": "string" + }, + "order": { + "type": "string", + "enum": [ + "null", + "ascending", + "descending" + ] + }, + "orderBy": { + "type": "string", + "enum": [ + "name", + "created_at" + ] + }, + "page": { + "type": "integer" + }, + "pageSize": { + "type": "integer" + }, + "type": { + "type": "string" + } + } + }, + "dto.DatabaseUpdate": { + "type": "object", + "required": [ + "type", + "username", + "version" + ], + "properties": { + "address": { + "type": "string" + }, + "clientCert": { + "type": "string" + }, + "clientKey": { + "type": "string" + }, + "description": { + "type": "string" + }, + "id": { + "type": "integer" + }, + "password": { + "type": "string" + }, + "port": { + "type": "integer" + }, + "rootCert": { + "type": "string" + }, + "skipVerify": { + "type": "boolean" + }, + "ssl": { + "type": "boolean" + }, + "type": { + "type": "string" + }, + "username": { + "type": "string" + }, + "version": { + "type": "string" + } + } + }, + "dto.DeviceBaseInfo": { + "type": "object", + "properties": { + "dns": { + "type": "array", + "items": { + "type": "string" + } + }, + "hostname": { + "type": "string" + }, + "hosts": { + "type": "array", + "items": { + "$ref": "#/definitions/dto.HostHelper" + } + }, + "localTime": { + "type": "string" + }, + "maxSize": { + "type": "integer" + }, + "ntp": { + "type": "string" + }, + "swapDetails": { + "type": "array", + "items": { + "$ref": "#/definitions/dto.SwapHelper" + } + }, + "swapMemoryAvailable": { + "type": "integer" + }, + "swapMemoryTotal": { + "type": "integer" + }, + "swapMemoryUsed": { + "type": "integer" + }, + "timeZone": { + "type": "string" + }, + "user": { + "type": "string" + } + } + }, + "dto.DiskInfo": { + "type": "object", + "properties": { + "device": { + "type": "string" + }, + "free": { + "type": "integer" + }, + "inodesFree": { + "type": "integer" + }, + "inodesTotal": { + "type": "integer" + }, + "inodesUsed": { + "type": "integer" + }, + "inodesUsedPercent": { + "type": "number" + }, + "path": { + "type": "string" + }, + "total": { + "type": "integer" + }, + "type": { + "type": "string" + }, + "used": { + "type": "integer" + }, + "usedPercent": { + "type": "number" + } + } + }, + "dto.DockerOperation": { + "type": "object", + "required": [ + "operation" + ], + "properties": { + "operation": { + "type": "string", + "enum": [ + "start", + "restart", + "stop" + ] + } + } + }, + "dto.DownloadRecord": { + "type": "object", + "required": [ + "fileDir", + "fileName", + "source" + ], + "properties": { + "fileDir": { + "type": "string" + }, + "fileName": { + "type": "string" + }, + "source": { + "type": "string", + "enum": [ + "OSS", + "S3", + "SFTP", + "MINIO", + "LOCAL", + "COS", + "KODO", + "OneDrive", + "WebDAV" + ] + } + } + }, + "dto.Fail2BanBaseInfo": { + "type": "object", + "properties": { + "banAction": { + "type": "string" + }, + "banTime": { + "type": "string" + }, + "findTime": { + "type": "string" + }, + "isActive": { + "type": "boolean" + }, + "isEnable": { + "type": "boolean" + }, + "isExist": { + "type": "boolean" + }, + "logPath": { + "type": "string" + }, + "maxRetry": { + "type": "integer" + }, + "port": { + "type": "integer" + }, + "version": { + "type": "string" + } + } + }, + "dto.Fail2BanSearch": { + "type": "object", + "required": [ + "status" + ], + "properties": { + "status": { + "type": "string", + "enum": [ + "banned", + "ignore" + ] + } + } + }, + "dto.Fail2BanUpdate": { + "type": "object", + "required": [ + "key" + ], + "properties": { + "key": { + "type": "string", + "enum": [ + "port", + "bantime", + "findtime", + "maxretry", + "banaction", + "logpath", + "port" + ] + }, + "value": { + "type": "string" + } + } + }, + "dto.FirewallBaseInfo": { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "pingStatus": { + "type": "string" + }, + "status": { + "type": "string" + }, + "version": { + "type": "string" + } + } + }, + "dto.FirewallOperation": { + "type": "object", + "required": [ + "operation" + ], + "properties": { + "operation": { + "type": "string", + "enum": [ + "start", + "stop", + "restart", + "disablePing", + "enablePing" + ] + } + } + }, + "dto.ForBuckets": { + "type": "object", + "required": [ + "credential", + "type", + "vars" + ], + "properties": { + "accessKey": { + "type": "string" + }, + "credential": { + "type": "string" + }, + "type": { + "type": "string" + }, + "vars": { + "type": "string" + } + } + }, + "dto.ForwardRuleOperate": { + "type": "object", + "properties": { + "rules": { + "type": "array", + "items": { + "type": "object", + "required": [ + "operation", + "port", + "protocol", + "targetPort" + ], + "properties": { + "num": { + "type": "string" + }, + "operation": { + "type": "string", + "enum": [ + "add", + "remove" + ] + }, + "port": { + "type": "string" + }, + "protocol": { + "type": "string", + "enum": [ + "tcp", + "udp", + "tcp/udp" + ] + }, + "targetIP": { + "type": "string" + }, + "targetPort": { + "type": "string" + } + } + } + } + } + }, + "dto.FtpBaseInfo": { + "type": "object", + "properties": { + "isActive": { + "type": "boolean" + }, + "isExist": { + "type": "boolean" + } + } + }, + "dto.FtpCreate": { + "type": "object", + "required": [ + "password", + "path", + "user" + ], + "properties": { + "description": { + "type": "string" + }, + "password": { + "type": "string" + }, + "path": { + "type": "string" + }, + "user": { + "type": "string" + } + } + }, + "dto.FtpLogSearch": { + "type": "object", + "required": [ + "page", + "pageSize" + ], + "properties": { + "operation": { + "type": "string" + }, + "page": { + "type": "integer" + }, + "pageSize": { + "type": "integer" + }, + "user": { + "type": "string" + } + } + }, + "dto.FtpUpdate": { + "type": "object", + "required": [ + "password", + "path" + ], + "properties": { + "description": { + "type": "string" + }, + "id": { + "type": "integer" + }, + "password": { + "type": "string" + }, + "path": { + "type": "string" + }, + "status": { + "type": "string" + } + } + }, + "dto.GPUInfo": { + "type": "object", + "properties": { + "fanSpeed": { + "type": "string" + }, + "gpuUtil": { + "type": "string" + }, + "index": { + "type": "integer" + }, + "maxPowerLimit": { + "type": "string" + }, + "memTotal": { + "type": "string" + }, + "memUsed": { + "type": "string" + }, + "memoryUsage": { + "type": "string" + }, + "performanceState": { + "type": "string" + }, + "powerDraw": { + "type": "string" + }, + "powerUsage": { + "type": "string" + }, + "productName": { + "type": "string" + }, + "temperature": { + "type": "string" + } + } + }, + "dto.GenerateLoad": { + "type": "object", + "required": [ + "encryptionMode" + ], + "properties": { + "encryptionMode": { + "type": "string", + "enum": [ + "rsa", + "ed25519", + "ecdsa", + "dsa" + ] + } + } + }, + "dto.GenerateSSH": { + "type": "object", + "required": [ + "encryptionMode" + ], + "properties": { + "encryptionMode": { + "type": "string", + "enum": [ + "rsa", + "ed25519", + "ecdsa", + "dsa" + ] + }, + "password": { + "type": "string" + } + } + }, + "dto.GroupCreate": { + "type": "object", + "required": [ + "name", + "type" + ], + "properties": { + "id": { + "type": "integer" + }, + "name": { + "type": "string" + }, + "type": { + "type": "string" + } + } + }, + "dto.GroupInfo": { + "type": "object", + "properties": { + "id": { + "type": "integer" + }, + "isDefault": { + "type": "boolean" + }, + "name": { + "type": "string" + }, + "type": { + "type": "string" + } + } + }, + "dto.GroupSearch": { + "type": "object", + "required": [ + "type" + ], + "properties": { + "type": { + "type": "string" + } + } + }, + "dto.GroupUpdate": { + "type": "object", + "required": [ + "type" + ], + "properties": { + "id": { + "type": "integer" + }, + "isDefault": { + "type": "boolean" + }, + "name": { + "type": "string" + }, + "type": { + "type": "string" + } + } + }, + "dto.HostConnTest": { + "type": "object", + "required": [ + "addr", + "port", + "user" + ], + "properties": { + "addr": { + "type": "string" + }, + "authMode": { + "type": "string", + "enum": [ + "password", + "key" + ] + }, + "passPhrase": { + "type": "string" + }, + "password": { + "type": "string" + }, + "port": { + "type": "integer", + "maximum": 65535, + "minimum": 1 + }, + "privateKey": { + "type": "string" + }, + "user": { + "type": "string" + } + } + }, + "dto.HostHelper": { + "type": "object", + "properties": { + "host": { + "type": "string" + }, + "ip": { + "type": "string" + } + } + }, + "dto.HostOperate": { + "type": "object", + "required": [ + "addr", + "port", + "user" + ], + "properties": { + "addr": { + "type": "string" + }, + "authMode": { + "type": "string", + "enum": [ + "password", + "key" + ] + }, + "description": { + "type": "string" + }, + "groupID": { + "type": "integer" + }, + "id": { + "type": "integer" + }, + "name": { + "type": "string" + }, + "passPhrase": { + "type": "string" + }, + "password": { + "type": "string" + }, + "port": { + "type": "integer", + "maximum": 65535, + "minimum": 1 + }, + "privateKey": { + "type": "string" + }, + "rememberPassword": { + "type": "boolean" + }, + "user": { + "type": "string" + } + } + }, + "dto.HostTree": { + "type": "object", + "properties": { + "children": { + "type": "array", + "items": { + "$ref": "#/definitions/dto.TreeChild" + } + }, + "id": { + "type": "integer" + }, + "label": { + "type": "string" + } + } + }, + "dto.ImageBuild": { + "type": "object", + "required": [ + "dockerfile", + "from", + "name" + ], + "properties": { + "dockerfile": { + "type": "string" + }, + "from": { + "type": "string" + }, + "name": { + "type": "string" + }, + "tags": { + "type": "array", + "items": { + "type": "string" + } + } + } + }, + "dto.ImageInfo": { + "type": "object", + "properties": { + "createdAt": { + "type": "string" + }, + "id": { + "type": "string" + }, + "isUsed": { + "type": "boolean" + }, + "size": { + "type": "string" + }, + "tags": { + "type": "array", + "items": { + "type": "string" + } + } + } + }, + "dto.ImageLoad": { + "type": "object", + "required": [ + "path" + ], + "properties": { + "path": { + "type": "string" + } + } + }, + "dto.ImagePull": { + "type": "object", + "required": [ + "imageName" + ], + "properties": { + "imageName": { + "type": "string" + }, + "repoID": { + "type": "integer" + } + } + }, + "dto.ImagePush": { + "type": "object", + "required": [ + "name", + "repoID", + "tagName" + ], + "properties": { + "name": { + "type": "string" + }, + "repoID": { + "type": "integer" + }, + "tagName": { + "type": "string" + } + } + }, + "dto.ImageRepoDelete": { + "type": "object", + "required": [ + "ids" + ], + "properties": { + "ids": { + "type": "array", + "items": { + "type": "integer" + } + } + } + }, + "dto.ImageRepoOption": { + "type": "object", + "properties": { + "downloadUrl": { + "type": "string" + }, + "id": { + "type": "integer" + }, + "name": { + "type": "string" + } + } + }, + "dto.ImageRepoUpdate": { + "type": "object", + "properties": { + "auth": { + "type": "boolean" + }, + "downloadUrl": { + "type": "string" + }, + "id": { + "type": "integer" + }, + "password": { + "type": "string", + "maxLength": 256 + }, + "protocol": { + "type": "string" + }, + "username": { + "type": "string", + "maxLength": 256 + } + } + }, + "dto.ImageSave": { + "type": "object", + "required": [ + "name", + "path", + "tagName" + ], + "properties": { + "name": { + "type": "string" + }, + "path": { + "type": "string" + }, + "tagName": { + "type": "string" + } + } + }, + "dto.ImageTag": { + "type": "object", + "required": [ + "sourceID", + "targetName" + ], + "properties": { + "sourceID": { + "type": "string" + }, + "targetName": { + "type": "string" + } + } + }, + "dto.InspectReq": { + "type": "object", + "required": [ + "id", + "type" + ], + "properties": { + "id": { + "type": "string" + }, + "type": { + "type": "string" + } + } + }, + "dto.LogOption": { + "type": "object", + "properties": { + "logMaxFile": { + "type": "string" + }, + "logMaxSize": { + "type": "string" + } + } + }, + "dto.Login": { + "type": "object", + "required": [ + "authMethod", + "language", + "name", + "password" + ], + "properties": { + "authMethod": { + "type": "string", + "enum": [ + "jwt", + "session" + ] + }, + "captcha": { + "type": "string" + }, + "captchaID": { + "type": "string" + }, + "ignoreCaptcha": { + "type": "boolean" + }, + "language": { + "type": "string", + "enum": [ + "zh", + "en", + "tw" + ] + }, + "name": { + "type": "string" + }, + "password": { + "type": "string" + } + } + }, + "dto.MFALogin": { + "type": "object", + "required": [ + "code", + "name", + "password" + ], + "properties": { + "authMethod": { + "type": "string" + }, + "code": { + "type": "string" + }, + "name": { + "type": "string" + }, + "password": { + "type": "string" + } + } + }, + "dto.MfaCredential": { + "type": "object", + "required": [ + "code", + "interval", + "secret" + ], + "properties": { + "code": { + "type": "string" + }, + "interval": { + "type": "string" + }, + "secret": { + "type": "string" + } + } + }, + "dto.MonitorSearch": { + "type": "object", + "required": [ + "param" + ], + "properties": { + "endTime": { + "type": "string" + }, + "info": { + "type": "string" + }, + "param": { + "type": "string", + "enum": [ + "all", + "cpu", + "memory", + "load", + "io", + "network" + ] + }, + "startTime": { + "type": "string" + } + } + }, + "dto.MysqlDBCreate": { + "type": "object", + "required": [ + "database", + "format", + "from", + "name", + "password", + "permission", + "username" + ], + "properties": { + "database": { + "type": "string" + }, + "description": { + "type": "string" + }, + "format": { + "type": "string", + "enum": [ + "utf8mb4", + "utf8", + "gbk", + "big5" + ] + }, + "from": { + "type": "string", + "enum": [ + "local", + "remote" + ] + }, + "name": { + "type": "string" + }, + "password": { + "type": "string" + }, + "permission": { + "type": "string" + }, + "username": { + "type": "string" + } + } + }, + "dto.MysqlDBDelete": { + "type": "object", + "required": [ + "database", + "id", + "type" + ], + "properties": { + "database": { + "type": "string" + }, + "deleteBackup": { + "type": "boolean" + }, + "forceDelete": { + "type": "boolean" + }, + "id": { + "type": "integer" + }, + "type": { + "type": "string", + "enum": [ + "mysql", + "mariadb" + ] + } + } + }, + "dto.MysqlDBDeleteCheck": { + "type": "object", + "required": [ + "database", + "id", + "type" + ], + "properties": { + "database": { + "type": "string" + }, + "id": { + "type": "integer" + }, + "type": { + "type": "string", + "enum": [ + "mysql", + "mariadb" + ] + } + } + }, + "dto.MysqlDBSearch": { + "type": "object", + "required": [ + "database", + "order", + "orderBy", + "page", + "pageSize" + ], + "properties": { + "database": { + "type": "string" + }, + "info": { + "type": "string" + }, + "order": { + "type": "string", + "enum": [ + "null", + "ascending", + "descending" + ] + }, + "orderBy": { + "type": "string", + "enum": [ + "name", + "created_at" + ] + }, + "page": { + "type": "integer" + }, + "pageSize": { + "type": "integer" + } + } + }, + "dto.MysqlLoadDB": { + "type": "object", + "required": [ + "database", + "from", + "type" + ], + "properties": { + "database": { + "type": "string" + }, + "from": { + "type": "string", + "enum": [ + "local", + "remote" + ] + }, + "type": { + "type": "string", + "enum": [ + "mysql", + "mariadb" + ] + } + } + }, + "dto.MysqlOption": { + "type": "object", + "properties": { + "database": { + "type": "string" + }, + "from": { + "type": "string" + }, + "id": { + "type": "integer" + }, + "name": { + "type": "string" + }, + "type": { + "type": "string" + } + } + }, + "dto.MysqlStatus": { + "type": "object", + "properties": { + "Aborted_clients": { + "type": "string" + }, + "Aborted_connects": { + "type": "string" + }, + "Bytes_received": { + "type": "string" + }, + "Bytes_sent": { + "type": "string" + }, + "Com_commit": { + "type": "string" + }, + "Com_rollback": { + "type": "string" + }, + "Connections": { + "type": "string" + }, + "Created_tmp_disk_tables": { + "type": "string" + }, + "Created_tmp_tables": { + "type": "string" + }, + "File": { + "type": "string" + }, + "Innodb_buffer_pool_pages_dirty": { + "type": "string" + }, + "Innodb_buffer_pool_read_requests": { + "type": "string" + }, + "Innodb_buffer_pool_reads": { + "type": "string" + }, + "Key_read_requests": { + "type": "string" + }, + "Key_reads": { + "type": "string" + }, + "Key_write_requests": { + "type": "string" + }, + "Key_writes": { + "type": "string" + }, + "Max_used_connections": { + "type": "string" + }, + "Open_tables": { + "type": "string" + }, + "Opened_files": { + "type": "string" + }, + "Opened_tables": { + "type": "string" + }, + "Position": { + "type": "string" + }, + "Qcache_hits": { + "type": "string" + }, + "Qcache_inserts": { + "type": "string" + }, + "Questions": { + "type": "string" + }, + "Run": { + "type": "string" + }, + "Select_full_join": { + "type": "string" + }, + "Select_range_check": { + "type": "string" + }, + "Sort_merge_passes": { + "type": "string" + }, + "Table_locks_waited": { + "type": "string" + }, + "Threads_cached": { + "type": "string" + }, + "Threads_connected": { + "type": "string" + }, + "Threads_created": { + "type": "string" + }, + "Threads_running": { + "type": "string" + }, + "Uptime": { + "type": "string" + } + } + }, + "dto.MysqlVariables": { + "type": "object", + "properties": { + "binlog_cache_size": { + "type": "string" + }, + "innodb_buffer_pool_size": { + "type": "string" + }, + "innodb_log_buffer_size": { + "type": "string" + }, + "join_buffer_size": { + "type": "string" + }, + "key_buffer_size": { + "type": "string" + }, + "long_query_time": { + "type": "string" + }, + "max_connections": { + "type": "string" + }, + "max_heap_table_size": { + "type": "string" + }, + "query_cache_size": { + "type": "string" + }, + "query_cache_type": { + "type": "string" + }, + "read_buffer_size": { + "type": "string" + }, + "read_rnd_buffer_size": { + "type": "string" + }, + "slow_query_log": { + "type": "string" + }, + "sort_buffer_size": { + "type": "string" + }, + "table_open_cache": { + "type": "string" + }, + "thread_cache_size": { + "type": "string" + }, + "thread_stack": { + "type": "string" + }, + "tmp_table_size": { + "type": "string" + } + } + }, + "dto.MysqlVariablesUpdate": { + "type": "object", + "required": [ + "database", + "type" + ], + "properties": { + "database": { + "type": "string" + }, + "type": { + "type": "string", + "enum": [ + "mysql", + "mariadb" + ] + }, + "variables": { + "type": "array", + "items": { + "$ref": "#/definitions/dto.MysqlVariablesUpdateHelper" + } + } + } + }, + "dto.MysqlVariablesUpdateHelper": { + "type": "object", + "properties": { + "param": { + "type": "string" + }, + "value": {} + } + }, + "dto.NetworkCreate": { + "type": "object", + "required": [ + "driver", + "name" + ], + "properties": { + "auxAddress": { + "type": "array", + "items": { + "$ref": "#/definitions/dto.SettingUpdate" + } + }, + "auxAddressV6": { + "type": "array", + "items": { + "$ref": "#/definitions/dto.SettingUpdate" + } + }, + "driver": { + "type": "string" + }, + "gateway": { + "type": "string" + }, + "gatewayV6": { + "type": "string" + }, + "ipRange": { + "type": "string" + }, + "ipRangeV6": { + "type": "string" + }, + "ipv4": { + "type": "boolean" + }, + "ipv6": { + "type": "boolean" + }, + "labels": { + "type": "array", + "items": { + "type": "string" + } + }, + "name": { + "type": "string" + }, + "options": { + "type": "array", + "items": { + "type": "string" + } + }, + "subnet": { + "type": "string" + }, + "subnetV6": { + "type": "string" + } + } + }, + "dto.NginxKey": { + "type": "string", + "enum": [ + "index", + "limit-conn", + "ssl", + "cache", + "http-per", + "proxy-cache" + ], + "x-enum-varnames": [ + "Index", + "LimitConn", + "SSL", + "CACHE", + "HttpPer", + "ProxyCache" + ] + }, + "dto.OneDriveInfo": { + "type": "object", + "properties": { + "client_id": { + "type": "string" + }, + "client_secret": { + "type": "string" + }, + "redirect_uri": { + "type": "string" + } + } + }, + "dto.Operate": { + "type": "object", + "required": [ + "operation" + ], + "properties": { + "operation": { + "type": "string" + } + } + }, + "dto.OperateByID": { + "type": "object", + "required": [ + "id" + ], + "properties": { + "id": { + "type": "integer" + } + } + }, + "dto.OperationWithName": { + "type": "object", + "required": [ + "name" + ], + "properties": { + "name": { + "type": "string" + } + } + }, + "dto.OperationWithNameAndType": { + "type": "object", + "required": [ + "type" + ], + "properties": { + "name": { + "type": "string" + }, + "type": { + "type": "string" + } + } + }, + "dto.Options": { + "type": "object", + "properties": { + "option": { + "type": "string" + } + } + }, + "dto.OsInfo": { + "type": "object", + "properties": { + "diskSize": { + "type": "integer" + }, + "kernelArch": { + "type": "string" + }, + "kernelVersion": { + "type": "string" + }, + "os": { + "type": "string" + }, + "platform": { + "type": "string" + }, + "platformFamily": { + "type": "string" + } + } + }, + "dto.PageContainer": { + "type": "object", + "required": [ + "order", + "orderBy", + "page", + "pageSize", + "state" + ], + "properties": { + "excludeAppStore": { + "type": "boolean" + }, + "filters": { + "type": "string" + }, + "name": { + "type": "string" + }, + "order": { + "type": "string", + "enum": [ + "null", + "ascending", + "descending" + ] + }, + "orderBy": { + "type": "string", + "enum": [ + "name", + "state", + "created_at" + ] + }, + "page": { + "type": "integer" + }, + "pageSize": { + "type": "integer" + }, + "state": { + "type": "string", + "enum": [ + "all", + "created", + "running", + "paused", + "restarting", + "removing", + "exited", + "dead" + ] + } + } + }, + "dto.PageCronjob": { + "type": "object", + "required": [ + "order", + "orderBy", + "page", + "pageSize" + ], + "properties": { + "info": { + "type": "string" + }, + "order": { + "type": "string", + "enum": [ + "null", + "ascending", + "descending" + ] + }, + "orderBy": { + "type": "string", + "enum": [ + "name", + "status", + "created_at" + ] + }, + "page": { + "type": "integer" + }, + "pageSize": { + "type": "integer" + } + } + }, + "dto.PageInfo": { + "type": "object", + "required": [ + "page", + "pageSize" + ], + "properties": { + "page": { + "type": "integer" + }, + "pageSize": { + "type": "integer" + } + } + }, + "dto.PageResult": { + "type": "object", + "properties": { + "items": {}, + "total": { + "type": "integer" + } + } + }, + "dto.PasswordUpdate": { + "type": "object", + "required": [ + "newPassword", + "oldPassword" + ], + "properties": { + "newPassword": { + "type": "string" + }, + "oldPassword": { + "type": "string" + } + } + }, + "dto.PortHelper": { + "type": "object", + "properties": { + "containerPort": { + "type": "string" + }, + "hostIP": { + "type": "string" + }, + "hostPort": { + "type": "string" + }, + "protocol": { + "type": "string" + } + } + }, + "dto.PortRuleOperate": { + "type": "object", + "required": [ + "operation", + "port", + "protocol", + "strategy" + ], + "properties": { + "address": { + "type": "string" + }, + "description": { + "type": "string" + }, + "operation": { + "type": "string", + "enum": [ + "add", + "remove" + ] + }, + "port": { + "type": "string" + }, + "protocol": { + "type": "string", + "enum": [ + "tcp", + "udp", + "tcp/udp" + ] + }, + "strategy": { + "type": "string", + "enum": [ + "accept", + "drop" + ] + } + } + }, + "dto.PortRuleUpdate": { + "type": "object", + "properties": { + "newRule": { + "$ref": "#/definitions/dto.PortRuleOperate" + }, + "oldRule": { + "$ref": "#/definitions/dto.PortRuleOperate" + } + } + }, + "dto.PortUpdate": { + "type": "object", + "required": [ + "serverPort" + ], + "properties": { + "serverPort": { + "type": "integer", + "maximum": 65535, + "minimum": 1 + } + } + }, + "dto.PostgresqlBindUser": { + "type": "object", + "required": [ + "database", + "name", + "password", + "username" + ], + "properties": { + "database": { + "type": "string" + }, + "name": { + "type": "string" + }, + "password": { + "type": "string" + }, + "superUser": { + "type": "boolean" + }, + "username": { + "type": "string" + } + } + }, + "dto.PostgresqlDBCreate": { + "type": "object", + "required": [ + "database", + "from", + "name", + "password", + "username" + ], + "properties": { + "database": { + "type": "string" + }, + "description": { + "type": "string" + }, + "format": { + "type": "string" + }, + "from": { + "type": "string", + "enum": [ + "local", + "remote" + ] + }, + "name": { + "type": "string" + }, + "password": { + "type": "string" + }, + "superUser": { + "type": "boolean" + }, + "username": { + "type": "string" + } + } + }, + "dto.PostgresqlDBDelete": { + "type": "object", + "required": [ + "database", + "id", + "type" + ], + "properties": { + "database": { + "type": "string" + }, + "deleteBackup": { + "type": "boolean" + }, + "forceDelete": { + "type": "boolean" + }, + "id": { + "type": "integer" + }, + "type": { + "type": "string", + "enum": [ + "postgresql" + ] + } + } + }, + "dto.PostgresqlDBDeleteCheck": { + "type": "object", + "required": [ + "database", + "id", + "type" + ], + "properties": { + "database": { + "type": "string" + }, + "id": { + "type": "integer" + }, + "type": { + "type": "string", + "enum": [ + "postgresql" + ] + } + } + }, + "dto.PostgresqlDBSearch": { + "type": "object", + "required": [ + "database", + "order", + "orderBy", + "page", + "pageSize" + ], + "properties": { + "database": { + "type": "string" + }, + "info": { + "type": "string" + }, + "order": { + "type": "string", + "enum": [ + "null", + "ascending", + "descending" + ] + }, + "orderBy": { + "type": "string", + "enum": [ + "name", + "created_at" + ] + }, + "page": { + "type": "integer" + }, + "pageSize": { + "type": "integer" + } + } + }, + "dto.PostgresqlLoadDB": { + "type": "object", + "required": [ + "database", + "from", + "type" + ], + "properties": { + "database": { + "type": "string" + }, + "from": { + "type": "string", + "enum": [ + "local", + "remote" + ] + }, + "type": { + "type": "string", + "enum": [ + "postgresql" + ] + } + } + }, + "dto.ProxyUpdate": { + "type": "object", + "properties": { + "proxyPasswd": { + "type": "string" + }, + "proxyPasswdKeep": { + "type": "string" + }, + "proxyPort": { + "type": "string" + }, + "proxyType": { + "type": "string" + }, + "proxyUrl": { + "type": "string" + }, + "proxyUser": { + "type": "string" + } + } + }, + "dto.RecordSearch": { + "type": "object", + "required": [ + "page", + "pageSize", + "type" + ], + "properties": { + "detailName": { + "type": "string" + }, + "name": { + "type": "string" + }, + "page": { + "type": "integer" + }, + "pageSize": { + "type": "integer" + }, + "type": { + "type": "string" + } + } + }, + "dto.RecordSearchByCronjob": { + "type": "object", + "required": [ + "cronjobID", + "page", + "pageSize" + ], + "properties": { + "cronjobID": { + "type": "integer" + }, + "page": { + "type": "integer" + }, + "pageSize": { + "type": "integer" + } + } + }, + "dto.RedisCommand": { + "type": "object", + "properties": { + "command": { + "type": "string" + }, + "id": { + "type": "integer" + }, + "name": { + "type": "string" + } + } + }, + "dto.RedisConf": { + "type": "object", + "required": [ + "database" + ], + "properties": { + "containerName": { + "type": "string" + }, + "database": { + "type": "string" + }, + "maxclients": { + "type": "string" + }, + "maxmemory": { + "type": "string" + }, + "name": { + "type": "string" + }, + "port": { + "type": "integer" + }, + "requirepass": { + "type": "string" + }, + "timeout": { + "type": "string" + } + } + }, + "dto.RedisConfPersistenceUpdate": { + "type": "object", + "required": [ + "database", + "type" + ], + "properties": { + "appendfsync": { + "type": "string" + }, + "appendonly": { + "type": "string" + }, + "database": { + "type": "string" + }, + "save": { + "type": "string" + }, + "type": { + "type": "string", + "enum": [ + "aof", + "rbd" + ] + } + } + }, + "dto.RedisConfUpdate": { + "type": "object", + "required": [ + "database" + ], + "properties": { + "database": { + "type": "string" + }, + "maxclients": { + "type": "string" + }, + "maxmemory": { + "type": "string" + }, + "timeout": { + "type": "string" + } + } + }, + "dto.RedisPersistence": { + "type": "object", + "required": [ + "database" + ], + "properties": { + "appendfsync": { + "type": "string" + }, + "appendonly": { + "type": "string" + }, + "database": { + "type": "string" + }, + "save": { + "type": "string" + } + } + }, + "dto.RedisStatus": { + "type": "object", + "required": [ + "database" + ], + "properties": { + "connected_clients": { + "type": "string" + }, + "database": { + "type": "string" + }, + "instantaneous_ops_per_sec": { + "type": "string" + }, + "keyspace_hits": { + "type": "string" + }, + "keyspace_misses": { + "type": "string" + }, + "latest_fork_usec": { + "type": "string" + }, + "mem_fragmentation_ratio": { + "type": "string" + }, + "tcp_port": { + "type": "string" + }, + "total_commands_processed": { + "type": "string" + }, + "total_connections_received": { + "type": "string" + }, + "uptime_in_days": { + "type": "string" + }, + "used_memory": { + "type": "string" + }, + "used_memory_peak": { + "type": "string" + }, + "used_memory_rss": { + "type": "string" + } + } + }, + "dto.ResourceLimit": { + "type": "object", + "properties": { + "cpu": { + "type": "integer" + }, + "memory": { + "type": "integer" + } + } + }, + "dto.RuleSearch": { + "type": "object", + "required": [ + "page", + "pageSize", + "type" + ], + "properties": { + "info": { + "type": "string" + }, + "page": { + "type": "integer" + }, + "pageSize": { + "type": "integer" + }, + "status": { + "type": "string" + }, + "strategy": { + "type": "string" + }, + "type": { + "type": "string" + } + } + }, + "dto.SSHConf": { + "type": "object", + "properties": { + "file": { + "type": "string" + } + } + }, + "dto.SSHHistory": { + "type": "object", + "properties": { + "address": { + "type": "string" + }, + "area": { + "type": "string" + }, + "authMode": { + "type": "string" + }, + "date": { + "type": "string" + }, + "dateStr": { + "type": "string" + }, + "message": { + "type": "string" + }, + "port": { + "type": "string" + }, + "status": { + "type": "string" + }, + "user": { + "type": "string" + } + } + }, + "dto.SSHInfo": { + "type": "object", + "properties": { + "autoStart": { + "type": "boolean" + }, + "listenAddress": { + "type": "string" + }, + "message": { + "type": "string" + }, + "passwordAuthentication": { + "type": "string" + }, + "permitRootLogin": { + "type": "string" + }, + "port": { + "type": "string" + }, + "pubkeyAuthentication": { + "type": "string" + }, + "status": { + "type": "string" + }, + "useDNS": { + "type": "string" + } + } + }, + "dto.SSHLog": { + "type": "object", + "properties": { + "failedCount": { + "type": "integer" + }, + "logs": { + "type": "array", + "items": { + "$ref": "#/definitions/dto.SSHHistory" + } + }, + "successfulCount": { + "type": "integer" + }, + "totalCount": { + "type": "integer" + } + } + }, + "dto.SSHUpdate": { + "type": "object", + "required": [ + "key" + ], + "properties": { + "key": { + "type": "string" + }, + "newValue": { + "type": "string" + }, + "oldValue": { + "type": "string" + } + } + }, + "dto.SSLUpdate": { + "type": "object", + "required": [ + "ssl", + "sslType" + ], + "properties": { + "cert": { + "type": "string" + }, + "domain": { + "type": "string" + }, + "key": { + "type": "string" + }, + "ssl": { + "type": "string", + "enum": [ + "enable", + "disable" + ] + }, + "sslID": { + "type": "integer" + }, + "sslType": { + "type": "string", + "enum": [ + "self", + "select", + "import", + "import-paste", + "import-local" + ] + } + } + }, + "dto.SearchForTree": { + "type": "object", + "properties": { + "info": { + "type": "string" + } + } + }, + "dto.SearchHostWithPage": { + "type": "object", + "required": [ + "page", + "pageSize" + ], + "properties": { + "groupID": { + "type": "integer" + }, + "info": { + "type": "string" + }, + "page": { + "type": "integer" + }, + "pageSize": { + "type": "integer" + } + } + }, + "dto.SearchLgLogWithPage": { + "type": "object", + "required": [ + "page", + "pageSize" + ], + "properties": { + "ip": { + "type": "string" + }, + "page": { + "type": "integer" + }, + "pageSize": { + "type": "integer" + }, + "status": { + "type": "string" + } + } + }, + "dto.SearchOpLogWithPage": { + "type": "object", + "required": [ + "page", + "pageSize" + ], + "properties": { + "operation": { + "type": "string" + }, + "page": { + "type": "integer" + }, + "pageSize": { + "type": "integer" + }, + "source": { + "type": "string" + }, + "status": { + "type": "string" + } + } + }, + "dto.SearchRecord": { + "type": "object", + "required": [ + "page", + "pageSize" + ], + "properties": { + "cronjobID": { + "type": "integer" + }, + "endTime": { + "type": "string" + }, + "page": { + "type": "integer" + }, + "pageSize": { + "type": "integer" + }, + "startTime": { + "type": "string" + }, + "status": { + "type": "string" + } + } + }, + "dto.SearchSSHLog": { + "type": "object", + "required": [ + "Status", + "page", + "pageSize" + ], + "properties": { + "Status": { + "type": "string", + "enum": [ + "Success", + "Failed", + "All" + ] + }, + "info": { + "type": "string" + }, + "page": { + "type": "integer" + }, + "pageSize": { + "type": "integer" + } + } + }, + "dto.SearchWithPage": { + "type": "object", + "required": [ + "page", + "pageSize" + ], + "properties": { + "info": { + "type": "string" + }, + "page": { + "type": "integer" + }, + "pageSize": { + "type": "integer" + } + } + }, + "dto.SettingInfo": { + "type": "object", + "properties": { + "allowIPs": { + "type": "string" + }, + "appStoreLastModified": { + "type": "string" + }, + "appStoreSyncStatus": { + "type": "string" + }, + "appStoreVersion": { + "type": "string" + }, + "bindAddress": { + "type": "string" + }, + "bindDomain": { + "type": "string" + }, + "complexityVerification": { + "type": "string" + }, + "defaultNetwork": { + "type": "string" + }, + "developerMode": { + "type": "string" + }, + "dingVars": { + "type": "string" + }, + "dockerSockPath": { + "type": "string" + }, + "email": { + "type": "string" + }, + "emailVars": { + "type": "string" + }, + "expirationDays": { + "type": "string" + }, + "expirationTime": { + "type": "string" + }, + "fileRecycleBin": { + "type": "string" + }, + "ipv6": { + "type": "string" + }, + "language": { + "type": "string" + }, + "lastCleanData": { + "type": "string" + }, + "lastCleanSize": { + "type": "string" + }, + "lastCleanTime": { + "type": "string" + }, + "localTime": { + "type": "string" + }, + "menuTabs": { + "type": "string" + }, + "messageType": { + "type": "string" + }, + "mfaInterval": { + "type": "string" + }, + "mfaSecret": { + "type": "string" + }, + "mfaStatus": { + "type": "string" + }, + "monitorInterval": { + "type": "string" + }, + "monitorStatus": { + "type": "string" + }, + "monitorStoreDays": { + "type": "string" + }, + "noAuthSetting": { + "type": "string" + }, + "ntpSite": { + "type": "string" + }, + "panelName": { + "type": "string" + }, + "port": { + "type": "string" + }, + "proxyPasswd": { + "type": "string" + }, + "proxyPasswdKeep": { + "type": "string" + }, + "proxyPort": { + "type": "string" + }, + "proxyType": { + "type": "string" + }, + "proxyUrl": { + "type": "string" + }, + "proxyUser": { + "type": "string" + }, + "securityEntrance": { + "type": "string" + }, + "serverPort": { + "type": "string" + }, + "sessionTimeout": { + "type": "string" + }, + "snapshotIgnore": { + "type": "string" + }, + "ssl": { + "type": "string" + }, + "sslType": { + "type": "string" + }, + "systemIP": { + "type": "string" + }, + "systemVersion": { + "type": "string" + }, + "theme": { + "type": "string" + }, + "timeZone": { + "type": "string" + }, + "userName": { + "type": "string" + }, + "weChatVars": { + "type": "string" + }, + "xpackHideMenu": { + "type": "string" + } + } + }, + "dto.SettingUpdate": { + "type": "object", + "required": [ + "key" + ], + "properties": { + "key": { + "type": "string" + }, + "value": { + "type": "string" + } + } + }, + "dto.SnapshotBatchDelete": { + "type": "object", + "required": [ + "ids" + ], + "properties": { + "deleteWithFile": { + "type": "boolean" + }, + "ids": { + "type": "array", + "items": { + "type": "integer" + } + } + } + }, + "dto.SnapshotCreate": { + "type": "object", + "required": [ + "defaultDownload", + "from" + ], + "properties": { + "defaultDownload": { + "type": "string" + }, + "description": { + "type": "string", + "maxLength": 256 + }, + "from": { + "type": "string" + }, + "id": { + "type": "integer" + }, + "secret": { + "type": "string" + } + } + }, + "dto.SnapshotImport": { + "type": "object", + "properties": { + "description": { + "type": "string", + "maxLength": 256 + }, + "from": { + "type": "string" + }, + "names": { + "type": "array", + "items": { + "type": "string" + } + } + } + }, + "dto.SnapshotRecover": { + "type": "object", + "required": [ + "id" + ], + "properties": { + "id": { + "type": "integer" + }, + "isNew": { + "type": "boolean" + }, + "reDownload": { + "type": "boolean" + }, + "secret": { + "type": "string" + } + } + }, + "dto.SwapHelper": { + "type": "object", + "required": [ + "path" + ], + "properties": { + "isNew": { + "type": "boolean" + }, + "path": { + "type": "string" + }, + "size": { + "type": "integer" + }, + "used": { + "type": "string" + } + } + }, + "dto.TreeChild": { + "type": "object", + "properties": { + "id": { + "type": "integer" + }, + "label": { + "type": "string" + } + } + }, + "dto.UpdateByFile": { + "type": "object", + "properties": { + "file": { + "type": "string" + } + } + }, + "dto.UpdateByNameAndFile": { + "type": "object", + "properties": { + "file": { + "type": "string" + }, + "name": { + "type": "string" + } + } + }, + "dto.UpdateDescription": { + "type": "object", + "required": [ + "id" + ], + "properties": { + "description": { + "type": "string", + "maxLength": 256 + }, + "id": { + "type": "integer" + } + } + }, + "dto.UpdateFirewallDescription": { + "type": "object", + "required": [ + "strategy" + ], + "properties": { + "address": { + "type": "string" + }, + "description": { + "type": "string" + }, + "port": { + "type": "string" + }, + "protocol": { + "type": "string" + }, + "strategy": { + "type": "string", + "enum": [ + "accept", + "drop" + ] + }, + "type": { + "type": "string" + } + } + }, + "dto.Upgrade": { + "type": "object", + "required": [ + "version" + ], + "properties": { + "version": { + "type": "string" + } + } + }, + "dto.UpgradeInfo": { + "type": "object", + "properties": { + "latestVersion": { + "type": "string" + }, + "newVersion": { + "type": "string" + }, + "releaseNote": { + "type": "string" + }, + "testVersion": { + "type": "string" + } + } + }, + "dto.UserLoginInfo": { + "type": "object", + "properties": { + "mfaStatus": { + "type": "string" + }, + "name": { + "type": "string" + }, + "token": { + "type": "string" + } + } + }, + "dto.VolumeCreate": { + "type": "object", + "required": [ + "driver", + "name" + ], + "properties": { + "driver": { + "type": "string" + }, + "labels": { + "type": "array", + "items": { + "type": "string" + } + }, + "name": { + "type": "string" + }, + "options": { + "type": "array", + "items": { + "type": "string" + } + } + } + }, + "dto.VolumeHelper": { + "type": "object", + "properties": { + "containerDir": { + "type": "string" + }, + "mode": { + "type": "string" + }, + "sourceDir": { + "type": "string" + }, + "type": { + "type": "string" + } + } + }, + "files.FileInfo": { + "type": "object", + "properties": { + "content": { + "type": "string" + }, + "extension": { + "type": "string" + }, + "favoriteID": { + "type": "integer" + }, + "gid": { + "type": "string" + }, + "group": { + "type": "string" + }, + "isDetail": { + "type": "boolean" + }, + "isDir": { + "type": "boolean" + }, + "isHidden": { + "type": "boolean" + }, + "isSymlink": { + "type": "boolean" + }, + "itemTotal": { + "type": "integer" + }, + "items": { + "type": "array", + "items": { + "$ref": "#/definitions/files.FileInfo" + } + }, + "linkPath": { + "type": "string" + }, + "mimeType": { + "type": "string" + }, + "modTime": { + "type": "string" + }, + "mode": { + "type": "string" + }, + "name": { + "type": "string" + }, + "path": { + "type": "string" + }, + "size": { + "type": "integer" + }, + "type": { + "type": "string" + }, + "uid": { + "type": "string" + }, + "updateTime": { + "type": "string" + }, + "user": { + "type": "string" + } + } + }, + "mfa.Otp": { + "type": "object", + "properties": { + "qrImage": { + "type": "string" + }, + "secret": { + "type": "string" + } + } + }, + "model.App": { + "type": "object", + "properties": { + "createdAt": { + "type": "string" + }, + "crossVersionUpdate": { + "type": "boolean" + }, + "document": { + "type": "string" + }, + "github": { + "type": "string" + }, + "icon": { + "type": "string" + }, + "id": { + "type": "integer" + }, + "key": { + "type": "string" + }, + "lastModified": { + "type": "integer" + }, + "limit": { + "type": "integer" + }, + "name": { + "type": "string" + }, + "readMe": { + "type": "string" + }, + "recommend": { + "type": "integer" + }, + "required": { + "type": "string" + }, + "resource": { + "type": "string" + }, + "shortDescEn": { + "type": "string" + }, + "shortDescZh": { + "type": "string" + }, + "status": { + "type": "string" + }, + "tags": { + "type": "array", + "items": { + "type": "string" + } + }, + "type": { + "type": "string" + }, + "updatedAt": { + "type": "string" + }, + "website": { + "type": "string" + } + } + }, + "model.AppInstall": { + "type": "object", + "properties": { + "app": { + "$ref": "#/definitions/model.App" + }, + "appDetailId": { + "type": "integer" + }, + "appId": { + "type": "integer" + }, + "containerName": { + "type": "string" + }, + "createdAt": { + "type": "string" + }, + "description": { + "type": "string" + }, + "dockerCompose": { + "type": "string" + }, + "env": { + "type": "string" + }, + "httpPort": { + "type": "integer" + }, + "httpsPort": { + "type": "integer" + }, + "id": { + "type": "integer" + }, + "message": { + "type": "string" + }, + "name": { + "type": "string" + }, + "param": { + "type": "string" + }, + "serviceName": { + "type": "string" + }, + "status": { + "type": "string" + }, + "updatedAt": { + "type": "string" + }, + "version": { + "type": "string" + } + } + }, + "model.Tag": { + "type": "object", + "properties": { + "createdAt": { + "type": "string" + }, + "id": { + "type": "integer" + }, + "key": { + "type": "string" + }, + "name": { + "type": "string" + }, + "sort": { + "type": "integer" + }, + "updatedAt": { + "type": "string" + } + } + }, + "model.Website": { + "type": "object", + "properties": { + "IPV6": { + "type": "boolean" + }, + "accessLog": { + "type": "boolean" + }, + "alias": { + "type": "string" + }, + "appInstallId": { + "type": "integer" + }, + "createdAt": { + "type": "string" + }, + "defaultServer": { + "type": "boolean" + }, + "domains": { + "type": "array", + "items": { + "$ref": "#/definitions/model.WebsiteDomain" + } + }, + "errorLog": { + "type": "boolean" + }, + "expireDate": { + "type": "string" + }, + "ftpId": { + "type": "integer" + }, + "group": { + "type": "string" + }, + "httpConfig": { + "type": "string" + }, + "id": { + "type": "integer" + }, + "primaryDomain": { + "type": "string" + }, + "protocol": { + "type": "string" + }, + "proxy": { + "type": "string" + }, + "proxyType": { + "type": "string" + }, + "remark": { + "type": "string" + }, + "rewrite": { + "type": "string" + }, + "runtimeID": { + "type": "integer" + }, + "siteDir": { + "type": "string" + }, + "status": { + "type": "string" + }, + "type": { + "type": "string" + }, + "updatedAt": { + "type": "string" + }, + "user": { + "type": "string" + }, + "webSiteGroupId": { + "type": "integer" + }, + "webSiteSSL": { + "$ref": "#/definitions/model.WebsiteSSL" + }, + "webSiteSSLId": { + "type": "integer" + } + } + }, + "model.WebsiteAcmeAccount": { + "type": "object", + "properties": { + "createdAt": { + "type": "string" + }, + "eabHmacKey": { + "type": "string" + }, + "eabKid": { + "type": "string" + }, + "email": { + "type": "string" + }, + "id": { + "type": "integer" + }, + "keyType": { + "type": "string" + }, + "type": { + "type": "string" + }, + "updatedAt": { + "type": "string" + }, + "url": { + "type": "string" + } + } + }, + "model.WebsiteDnsAccount": { + "type": "object", + "properties": { + "createdAt": { + "type": "string" + }, + "id": { + "type": "integer" + }, + "name": { + "type": "string" + }, + "type": { + "type": "string" + }, + "updatedAt": { + "type": "string" + } + } + }, + "model.WebsiteDomain": { + "type": "object", + "properties": { + "createdAt": { + "type": "string" + }, + "domain": { + "type": "string" + }, + "id": { + "type": "integer" + }, + "port": { + "type": "integer" + }, + "updatedAt": { + "type": "string" + }, + "websiteId": { + "type": "integer" + } + } + }, + "model.WebsiteSSL": { + "type": "object", + "properties": { + "acmeAccount": { + "$ref": "#/definitions/model.WebsiteAcmeAccount" + }, + "acmeAccountId": { + "type": "integer" + }, + "autoRenew": { + "type": "boolean" + }, + "caId": { + "type": "integer" + }, + "certURL": { + "type": "string" + }, + "createdAt": { + "type": "string" + }, + "description": { + "type": "string" + }, + "dir": { + "type": "string" + }, + "disableCNAME": { + "type": "boolean" + }, + "dnsAccount": { + "$ref": "#/definitions/model.WebsiteDnsAccount" + }, + "dnsAccountId": { + "type": "integer" + }, + "domains": { + "type": "string" + }, + "execShell": { + "type": "boolean" + }, + "expireDate": { + "type": "string" + }, + "id": { + "type": "integer" + }, + "keyType": { + "type": "string" + }, + "message": { + "type": "string" + }, + "nameserver1": { + "type": "string" + }, + "nameserver2": { + "type": "string" + }, + "organization": { + "type": "string" + }, + "pem": { + "type": "string" + }, + "primaryDomain": { + "type": "string" + }, + "privateKey": { + "type": "string" + }, + "provider": { + "type": "string" + }, + "pushDir": { + "type": "boolean" + }, + "shell": { + "type": "string" + }, + "skipDNS": { + "type": "boolean" + }, + "startDate": { + "type": "string" + }, + "status": { + "type": "string" + }, + "type": { + "type": "string" + }, + "updatedAt": { + "type": "string" + }, + "websites": { + "type": "array", + "items": { + "$ref": "#/definitions/model.Website" + } + } + } + }, + "request.AppInstallCreate": { + "type": "object", + "required": [ + "appDetailId", + "name" + ], + "properties": { + "advanced": { + "type": "boolean" + }, + "allowPort": { + "type": "boolean" + }, + "appDetailId": { + "type": "integer" + }, + "containerName": { + "type": "string" + }, + "cpuQuota": { + "type": "number" + }, + "dockerCompose": { + "type": "string" + }, + "editCompose": { + "type": "boolean" + }, + "hostMode": { + "type": "boolean" + }, + "memoryLimit": { + "type": "number" + }, + "memoryUnit": { + "type": "string" + }, + "name": { + "type": "string" + }, + "params": { + "type": "object", + "additionalProperties": true + }, + "pullImage": { + "type": "boolean" + }, + "services": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + } + }, + "request.AppInstalledIgnoreUpgrade": { + "type": "object", + "required": [ + "detailID", + "operate" + ], + "properties": { + "detailID": { + "type": "integer" + }, + "operate": { + "type": "string", + "enum": [ + "cancel", + "ignore" + ] + } + } + }, + "request.AppInstalledInfo": { + "type": "object", + "required": [ + "key" + ], + "properties": { + "key": { + "type": "string" + }, + "name": { + "type": "string" + } + } + }, + "request.AppInstalledOperate": { + "type": "object", + "required": [ + "installId", + "operate" + ], + "properties": { + "backup": { + "type": "boolean" + }, + "backupId": { + "type": "integer" + }, + "deleteBackup": { + "type": "boolean" + }, + "deleteDB": { + "type": "boolean" + }, + "detailId": { + "type": "integer" + }, + "dockerCompose": { + "type": "string" + }, + "forceDelete": { + "type": "boolean" + }, + "installId": { + "type": "integer" + }, + "operate": { + "type": "string" + }, + "pullImage": { + "type": "boolean" + } + } + }, + "request.AppInstalledSearch": { + "type": "object", + "required": [ + "page", + "pageSize" + ], + "properties": { + "all": { + "type": "boolean" + }, + "name": { + "type": "string" + }, + "page": { + "type": "integer" + }, + "pageSize": { + "type": "integer" + }, + "sync": { + "type": "boolean" + }, + "tags": { + "type": "array", + "items": { + "type": "string" + } + }, + "type": { + "type": "string" + }, + "unused": { + "type": "boolean" + }, + "update": { + "type": "boolean" + } + } + }, + "request.AppInstalledUpdate": { + "type": "object", + "required": [ + "installId", + "params" + ], + "properties": { + "advanced": { + "type": "boolean" + }, + "allowPort": { + "type": "boolean" + }, + "containerName": { + "type": "string" + }, + "cpuQuota": { + "type": "number" + }, + "dockerCompose": { + "type": "string" + }, + "editCompose": { + "type": "boolean" + }, + "hostMode": { + "type": "boolean" + }, + "installId": { + "type": "integer" + }, + "memoryLimit": { + "type": "number" + }, + "memoryUnit": { + "type": "string" + }, + "params": { + "type": "object", + "additionalProperties": true + }, + "pullImage": { + "type": "boolean" + } + } + }, + "request.AppSearch": { + "type": "object", + "required": [ + "page", + "pageSize" + ], + "properties": { + "name": { + "type": "string" + }, + "page": { + "type": "integer" + }, + "pageSize": { + "type": "integer" + }, + "recommend": { + "type": "boolean" + }, + "resource": { + "type": "string" + }, + "tags": { + "type": "array", + "items": { + "type": "string" + } + }, + "type": { + "type": "string" + } + } + }, + "request.DirSizeReq": { + "type": "object", + "required": [ + "path" + ], + "properties": { + "path": { + "type": "string" + } + } + }, + "request.ExposedPort": { + "type": "object", + "properties": { + "containerPort": { + "type": "integer" + }, + "hostPort": { + "type": "integer" + } + } + }, + "request.FavoriteCreate": { + "type": "object", + "required": [ + "path" + ], + "properties": { + "path": { + "type": "string" + } + } + }, + "request.FavoriteDelete": { + "type": "object", + "required": [ + "id" + ], + "properties": { + "id": { + "type": "integer" + } + } + }, + "request.FileBatchDelete": { + "type": "object", + "required": [ + "paths" + ], + "properties": { + "isDir": { + "type": "boolean" + }, + "paths": { + "type": "array", + "items": { + "type": "string" + } + } + } + }, + "request.FileCompress": { + "type": "object", + "required": [ + "dst", + "files", + "name", + "type" + ], + "properties": { + "dst": { + "type": "string" + }, + "files": { + "type": "array", + "items": { + "type": "string" + } + }, + "name": { + "type": "string" + }, + "replace": { + "type": "boolean" + }, + "secret": { + "type": "string" + }, + "type": { + "type": "string" + } + } + }, + "request.FileContentReq": { + "type": "object", + "required": [ + "path" + ], + "properties": { + "isDetail": { + "type": "boolean" + }, + "path": { + "type": "string" + } + } + }, + "request.FileCreate": { + "type": "object", + "required": [ + "path" + ], + "properties": { + "content": { + "type": "string" + }, + "isDir": { + "type": "boolean" + }, + "isLink": { + "type": "boolean" + }, + "isSymlink": { + "type": "boolean" + }, + "linkPath": { + "type": "string" + }, + "mode": { + "type": "integer" + }, + "path": { + "type": "string" + }, + "sub": { + "type": "boolean" + } + } + }, + "request.FileDeCompress": { + "type": "object", + "required": [ + "dst", + "path", + "type" + ], + "properties": { + "dst": { + "type": "string" + }, + "path": { + "type": "string" + }, + "secret": { + "type": "string" + }, + "type": { + "type": "string" + } + } + }, + "request.FileDelete": { + "type": "object", + "required": [ + "path" + ], + "properties": { + "forceDelete": { + "type": "boolean" + }, + "isDir": { + "type": "boolean" + }, + "path": { + "type": "string" + } + } + }, + "request.FileDownload": { + "type": "object", + "required": [ + "name", + "paths", + "type" + ], + "properties": { + "compress": { + "type": "boolean" + }, + "name": { + "type": "string" + }, + "paths": { + "type": "array", + "items": { + "type": "string" + } + }, + "type": { + "type": "string" + } + } + }, + "request.FileEdit": { + "type": "object", + "required": [ + "path" + ], + "properties": { + "content": { + "type": "string" + }, + "path": { + "type": "string" + } + } + }, + "request.FileMove": { + "type": "object", + "required": [ + "newPath", + "oldPaths", + "type" + ], + "properties": { + "cover": { + "type": "boolean" + }, + "name": { + "type": "string" + }, + "newPath": { + "type": "string" + }, + "oldPaths": { + "type": "array", + "items": { + "type": "string" + } + }, + "type": { + "type": "string" + } + } + }, + "request.FileOption": { + "type": "object", + "properties": { + "containSub": { + "type": "boolean" + }, + "dir": { + "type": "boolean" + }, + "expand": { + "type": "boolean" + }, + "isDetail": { + "type": "boolean" + }, + "page": { + "type": "integer" + }, + "pageSize": { + "type": "integer" + }, + "path": { + "type": "string" + }, + "search": { + "type": "string" + }, + "showHidden": { + "type": "boolean" + }, + "sortBy": { + "type": "string" + }, + "sortOrder": { + "type": "string" + } + } + }, + "request.FilePathCheck": { + "type": "object", + "required": [ + "path" + ], + "properties": { + "path": { + "type": "string" + } + } + }, + "request.FileReadByLineReq": { + "type": "object", + "required": [ + "page", + "pageSize", + "type" + ], + "properties": { + "ID": { + "type": "integer" + }, + "latest": { + "type": "boolean" + }, + "name": { + "type": "string" + }, + "page": { + "type": "integer" + }, + "pageSize": { + "type": "integer" + }, + "type": { + "type": "string" + } + } + }, + "request.FileRename": { + "type": "object", + "required": [ + "newName", + "oldName" + ], + "properties": { + "newName": { + "type": "string" + }, + "oldName": { + "type": "string" + } + } + }, + "request.FileRoleReq": { + "type": "object", + "required": [ + "group", + "mode", + "paths", + "user" + ], + "properties": { + "group": { + "type": "string" + }, + "mode": { + "type": "integer" + }, + "paths": { + "type": "array", + "items": { + "type": "string" + } + }, + "sub": { + "type": "boolean" + }, + "user": { + "type": "string" + } + } + }, + "request.FileRoleUpdate": { + "type": "object", + "required": [ + "group", + "path", + "user" + ], + "properties": { + "group": { + "type": "string" + }, + "path": { + "type": "string" + }, + "sub": { + "type": "boolean" + }, + "user": { + "type": "string" + } + } + }, + "request.FileWget": { + "type": "object", + "required": [ + "name", + "path", + "url" + ], + "properties": { + "ignoreCertificate": { + "type": "boolean" + }, + "name": { + "type": "string" + }, + "path": { + "type": "string" + }, + "url": { + "type": "string" + } + } + }, + "request.HostToolConfig": { + "type": "object", + "required": [ + "type" + ], + "properties": { + "content": { + "type": "string" + }, + "operate": { + "type": "string", + "enum": [ + "get", + "set" + ] + }, + "type": { + "type": "string", + "enum": [ + "supervisord" + ] + } + } + }, + "request.HostToolCreate": { + "type": "object", + "required": [ + "type" + ], + "properties": { + "configPath": { + "type": "string" + }, + "serviceName": { + "type": "string" + }, + "type": { + "type": "string" + } + } + }, + "request.HostToolLogReq": { + "type": "object", + "required": [ + "type" + ], + "properties": { + "type": { + "type": "string", + "enum": [ + "supervisord" + ] + } + } + }, + "request.HostToolReq": { + "type": "object", + "required": [ + "type" + ], + "properties": { + "operate": { + "type": "string", + "enum": [ + "status", + "restart", + "start", + "stop" + ] + }, + "type": { + "type": "string", + "enum": [ + "supervisord" + ] + } + } + }, + "request.NewAppInstall": { + "type": "object", + "properties": { + "advanced": { + "type": "boolean" + }, + "allowPort": { + "type": "boolean" + }, + "appDetailID": { + "type": "integer" + }, + "containerName": { + "type": "string" + }, + "cpuQuota": { + "type": "number" + }, + "dockerCompose": { + "type": "string" + }, + "editCompose": { + "type": "boolean" + }, + "hostMode": { + "type": "boolean" + }, + "memoryLimit": { + "type": "number" + }, + "memoryUnit": { + "type": "string" + }, + "name": { + "type": "string" + }, + "params": { + "type": "object", + "additionalProperties": true + }, + "pullImage": { + "type": "boolean" + } + } + }, + "request.NginxAntiLeechUpdate": { + "type": "object", + "required": [ + "extends", + "return", + "websiteID" + ], + "properties": { + "blocked": { + "type": "boolean" + }, + "cache": { + "type": "boolean" + }, + "cacheTime": { + "type": "integer" + }, + "cacheUint": { + "type": "string" + }, + "enable": { + "type": "boolean" + }, + "extends": { + "type": "string" + }, + "logEnable": { + "type": "boolean" + }, + "noneRef": { + "type": "boolean" + }, + "return": { + "type": "string" + }, + "serverNames": { + "type": "array", + "items": { + "type": "string" + } + }, + "websiteID": { + "type": "integer" + } + } + }, + "request.NginxAuthReq": { + "type": "object", + "required": [ + "websiteID" + ], + "properties": { + "websiteID": { + "type": "integer" + } + } + }, + "request.NginxAuthUpdate": { + "type": "object", + "required": [ + "operate", + "websiteID" + ], + "properties": { + "operate": { + "type": "string" + }, + "password": { + "type": "string" + }, + "remark": { + "type": "string" + }, + "username": { + "type": "string" + }, + "websiteID": { + "type": "integer" + } + } + }, + "request.NginxCommonReq": { + "type": "object", + "required": [ + "websiteID" + ], + "properties": { + "websiteID": { + "type": "integer" + } + } + }, + "request.NginxConfigFileUpdate": { + "type": "object", + "required": [ + "content" + ], + "properties": { + "backup": { + "type": "boolean" + }, + "content": { + "type": "string" + } + } + }, + "request.NginxConfigUpdate": { + "type": "object", + "required": [ + "operate" + ], + "properties": { + "operate": { + "type": "string", + "enum": [ + "add", + "update", + "delete" + ] + }, + "params": {}, + "scope": { + "$ref": "#/definitions/dto.NginxKey" + }, + "websiteId": { + "type": "integer" + } + } + }, + "request.NginxProxyUpdate": { + "type": "object", + "required": [ + "content", + "name", + "websiteID" + ], + "properties": { + "content": { + "type": "string" + }, + "name": { + "type": "string" + }, + "websiteID": { + "type": "integer" + } + } + }, + "request.NginxRedirectReq": { + "type": "object", + "required": [ + "name", + "operate", + "redirect", + "target", + "type", + "websiteID" + ], + "properties": { + "domains": { + "type": "array", + "items": { + "type": "string" + } + }, + "enable": { + "type": "boolean" + }, + "keepPath": { + "type": "boolean" + }, + "name": { + "type": "string" + }, + "operate": { + "type": "string" + }, + "path": { + "type": "string" + }, + "redirect": { + "type": "string" + }, + "redirectRoot": { + "type": "boolean" + }, + "target": { + "type": "string" + }, + "type": { + "type": "string" + }, + "websiteID": { + "type": "integer" + } + } + }, + "request.NginxRedirectUpdate": { + "type": "object", + "required": [ + "content", + "name", + "websiteID" + ], + "properties": { + "content": { + "type": "string" + }, + "name": { + "type": "string" + }, + "websiteID": { + "type": "integer" + } + } + }, + "request.NginxRewriteReq": { + "type": "object", + "required": [ + "name", + "websiteId" + ], + "properties": { + "name": { + "type": "string" + }, + "websiteId": { + "type": "integer" + } + } + }, + "request.NginxRewriteUpdate": { + "type": "object", + "required": [ + "name", + "websiteId" + ], + "properties": { + "content": { + "type": "string" + }, + "name": { + "type": "string" + }, + "websiteId": { + "type": "integer" + } + } + }, + "request.NginxScopeReq": { + "type": "object", + "required": [ + "scope" + ], + "properties": { + "scope": { + "$ref": "#/definitions/dto.NginxKey" + }, + "websiteId": { + "type": "integer" + } + } + }, + "request.NodeModuleReq": { + "type": "object", + "required": [ + "ID" + ], + "properties": { + "ID": { + "type": "integer" + } + } + }, + "request.NodePackageReq": { + "type": "object", + "properties": { + "codeDir": { + "type": "string" + } + } + }, + "request.PHPExtensionsCreate": { + "type": "object", + "required": [ + "extensions", + "name" + ], + "properties": { + "extensions": { + "type": "string" + }, + "name": { + "type": "string" + } + } + }, + "request.PHPExtensionsDelete": { + "type": "object", + "required": [ + "id" + ], + "properties": { + "id": { + "type": "integer" + } + } + }, + "request.PHPExtensionsSearch": { + "type": "object", + "required": [ + "page", + "pageSize" + ], + "properties": { + "all": { + "type": "boolean" + }, + "page": { + "type": "integer" + }, + "pageSize": { + "type": "integer" + } + } + }, + "request.PHPExtensionsUpdate": { + "type": "object", + "required": [ + "extensions", + "id" + ], + "properties": { + "extensions": { + "type": "string" + }, + "id": { + "type": "integer" + } + } + }, + "request.PortUpdate": { + "type": "object", + "properties": { + "key": { + "type": "string" + }, + "name": { + "type": "string" + }, + "port": { + "type": "integer" + } + } + }, + "request.ProcessReq": { + "type": "object", + "required": [ + "PID" + ], + "properties": { + "PID": { + "type": "integer" + } + } + }, + "request.RecycleBinReduce": { + "type": "object", + "required": [ + "from", + "rName" + ], + "properties": { + "from": { + "type": "string" + }, + "name": { + "type": "string" + }, + "rName": { + "type": "string" + } + } + }, + "request.RuntimeCreate": { + "type": "object", + "properties": { + "appDetailId": { + "type": "integer" + }, + "clean": { + "type": "boolean" + }, + "codeDir": { + "type": "string" + }, + "exposedPorts": { + "type": "array", + "items": { + "$ref": "#/definitions/request.ExposedPort" + } + }, + "image": { + "type": "string" + }, + "install": { + "type": "boolean" + }, + "name": { + "type": "string" + }, + "params": { + "type": "object", + "additionalProperties": true + }, + "port": { + "type": "integer" + }, + "resource": { + "type": "string" + }, + "source": { + "type": "string" + }, + "type": { + "type": "string" + }, + "version": { + "type": "string" + } + } + }, + "request.RuntimeDelete": { + "type": "object", + "properties": { + "forceDelete": { + "type": "boolean" + }, + "id": { + "type": "integer" + } + } + }, + "request.RuntimeOperate": { + "type": "object", + "properties": { + "ID": { + "type": "integer" + }, + "operate": { + "type": "string" + } + } + }, + "request.RuntimeSearch": { + "type": "object", + "required": [ + "page", + "pageSize" + ], + "properties": { + "name": { + "type": "string" + }, + "page": { + "type": "integer" + }, + "pageSize": { + "type": "integer" + }, + "status": { + "type": "string" + }, + "type": { + "type": "string" + } + } + }, + "request.RuntimeUpdate": { + "type": "object", + "properties": { + "clean": { + "type": "boolean" + }, + "codeDir": { + "type": "string" + }, + "exposedPorts": { + "type": "array", + "items": { + "$ref": "#/definitions/request.ExposedPort" + } + }, + "id": { + "type": "integer" + }, + "image": { + "type": "string" + }, + "install": { + "type": "boolean" + }, + "name": { + "type": "string" + }, + "params": { + "type": "object", + "additionalProperties": true + }, + "port": { + "type": "integer" + }, + "rebuild": { + "type": "boolean" + }, + "source": { + "type": "string" + }, + "version": { + "type": "string" + } + } + }, + "request.SearchUploadWithPage": { + "type": "object", + "required": [ + "page", + "pageSize", + "path" + ], + "properties": { + "page": { + "type": "integer" + }, + "pageSize": { + "type": "integer" + }, + "path": { + "type": "string" + } + } + }, + "request.SupervisorProcessConfig": { + "type": "object", + "properties": { + "command": { + "type": "string" + }, + "dir": { + "type": "string" + }, + "name": { + "type": "string" + }, + "numprocs": { + "type": "string" + }, + "operate": { + "type": "string" + }, + "user": { + "type": "string" + } + } + }, + "request.SupervisorProcessFileReq": { + "type": "object", + "required": [ + "file", + "name", + "operate" + ], + "properties": { + "content": { + "type": "string" + }, + "file": { + "type": "string", + "enum": [ + "out.log", + "err.log", + "config" + ] + }, + "name": { + "type": "string" + }, + "operate": { + "type": "string", + "enum": [ + "get", + "clear", + "update" + ] + } + } + }, + "request.WebsiteAcmeAccountCreate": { + "type": "object", + "required": [ + "email", + "keyType", + "type" + ], + "properties": { + "eabHmacKey": { + "type": "string" + }, + "eabKid": { + "type": "string" + }, + "email": { + "type": "string" + }, + "keyType": { + "type": "string", + "enum": [ + "P256", + "P384", + "2048", + "3072", + "4096", + "8192" + ] + }, + "type": { + "type": "string", + "enum": [ + "letsencrypt", + "zerossl", + "buypass", + "google" + ] + } + } + }, + "request.WebsiteBatchDelReq": { + "type": "object", + "required": [ + "ids" + ], + "properties": { + "ids": { + "type": "array", + "items": { + "type": "integer" + } + } + } + }, + "request.WebsiteCACreate": { + "type": "object", + "required": [ + "commonName", + "country", + "keyType", + "name", + "organization" + ], + "properties": { + "city": { + "type": "string" + }, + "commonName": { + "type": "string" + }, + "country": { + "type": "string" + }, + "keyType": { + "type": "string", + "enum": [ + "P256", + "P384", + "2048", + "3072", + "4096", + "8192" + ] + }, + "name": { + "type": "string" + }, + "organization": { + "type": "string" + }, + "organizationUint": { + "type": "string" + }, + "province": { + "type": "string" + } + } + }, + "request.WebsiteCAObtain": { + "type": "object", + "required": [ + "domains", + "id", + "keyType", + "time", + "unit" + ], + "properties": { + "autoRenew": { + "type": "boolean" + }, + "description": { + "type": "string" + }, + "dir": { + "type": "string" + }, + "domains": { + "type": "string" + }, + "execShell": { + "type": "boolean" + }, + "id": { + "type": "integer" + }, + "keyType": { + "type": "string", + "enum": [ + "P256", + "P384", + "2048", + "3072", + "4096", + "8192" + ] + }, + "pushDir": { + "type": "boolean" + }, + "renew": { + "type": "boolean" + }, + "shell": { + "type": "string" + }, + "sslID": { + "type": "integer" + }, + "time": { + "type": "integer" + }, + "unit": { + "type": "string" + } + } + }, + "request.WebsiteCASearch": { + "type": "object", + "required": [ + "page", + "pageSize" + ], + "properties": { + "page": { + "type": "integer" + }, + "pageSize": { + "type": "integer" + } + } + }, + "request.WebsiteCommonReq": { + "type": "object", + "required": [ + "id" + ], + "properties": { + "id": { + "type": "integer" + } + } + }, + "request.WebsiteCreate": { + "type": "object", + "required": [ + "alias", + "primaryDomain", + "type", + "webSiteGroupID" + ], + "properties": { + "IPV6": { + "type": "boolean" + }, + "alias": { + "type": "string" + }, + "appID": { + "type": "integer" + }, + "appInstall": { + "$ref": "#/definitions/request.NewAppInstall" + }, + "appInstallID": { + "type": "integer" + }, + "appType": { + "type": "string", + "enum": [ + "new", + "installed" + ] + }, + "ftpPassword": { + "type": "string" + }, + "ftpUser": { + "type": "string" + }, + "otherDomains": { + "type": "string" + }, + "port": { + "type": "integer" + }, + "primaryDomain": { + "type": "string" + }, + "proxy": { + "type": "string" + }, + "proxyType": { + "type": "string" + }, + "remark": { + "type": "string" + }, + "runtimeID": { + "type": "integer" + }, + "type": { + "type": "string" + }, + "webSiteGroupID": { + "type": "integer" + } + } + }, + "request.WebsiteDNSReq": { + "type": "object", + "required": [ + "acmeAccountId", + "domains" + ], + "properties": { + "acmeAccountId": { + "type": "integer" + }, + "domains": { + "type": "array", + "items": { + "type": "string" + } + } + } + }, + "request.WebsiteDefaultUpdate": { + "type": "object", + "properties": { + "id": { + "type": "integer" + } + } + }, + "request.WebsiteDelete": { + "type": "object", + "required": [ + "id" + ], + "properties": { + "deleteApp": { + "type": "boolean" + }, + "deleteBackup": { + "type": "boolean" + }, + "forceDelete": { + "type": "boolean" + }, + "id": { + "type": "integer" + } + } + }, + "request.WebsiteDnsAccountCreate": { + "type": "object", + "required": [ + "authorization", + "name", + "type" + ], + "properties": { + "authorization": { + "type": "object", + "additionalProperties": { + "type": "string" + } + }, + "name": { + "type": "string" + }, + "type": { + "type": "string" + } + } + }, + "request.WebsiteDnsAccountUpdate": { + "type": "object", + "required": [ + "authorization", + "id", + "name", + "type" + ], + "properties": { + "authorization": { + "type": "object", + "additionalProperties": { + "type": "string" + } + }, + "id": { + "type": "integer" + }, + "name": { + "type": "string" + }, + "type": { + "type": "string" + } + } + }, + "request.WebsiteDomainCreate": { + "type": "object", + "required": [ + "domains", + "websiteID" + ], + "properties": { + "domains": { + "type": "string" + }, + "websiteID": { + "type": "integer" + } + } + }, + "request.WebsiteDomainDelete": { + "type": "object", + "required": [ + "id" + ], + "properties": { + "id": { + "type": "integer" + } + } + }, + "request.WebsiteHTTPSOp": { + "type": "object", + "required": [ + "websiteId" + ], + "properties": { + "SSLProtocol": { + "type": "array", + "items": { + "type": "string" + } + }, + "algorithm": { + "type": "string" + }, + "certificate": { + "type": "string" + }, + "certificatePath": { + "type": "string" + }, + "enable": { + "type": "boolean" + }, + "hsts": { + "type": "boolean" + }, + "httpConfig": { + "type": "string", + "enum": [ + "HTTPSOnly", + "HTTPAlso", + "HTTPToHTTPS" + ] + }, + "importType": { + "type": "string" + }, + "privateKey": { + "type": "string" + }, + "privateKeyPath": { + "type": "string" + }, + "type": { + "type": "string", + "enum": [ + "existed", + "auto", + "manual" + ] + }, + "websiteId": { + "type": "integer" + }, + "websiteSSLId": { + "type": "integer" + } + } + }, + "request.WebsiteHtmlUpdate": { + "type": "object", + "required": [ + "content", + "type" + ], + "properties": { + "content": { + "type": "string" + }, + "type": { + "type": "string" + } + } + }, + "request.WebsiteInstallCheckReq": { + "type": "object", + "properties": { + "InstallIds": { + "type": "array", + "items": { + "type": "integer" + } + } + } + }, + "request.WebsiteLogReq": { + "type": "object", + "required": [ + "id", + "logType", + "operate" + ], + "properties": { + "id": { + "type": "integer" + }, + "logType": { + "type": "string" + }, + "operate": { + "type": "string" + }, + "page": { + "type": "integer" + }, + "pageSize": { + "type": "integer" + } + } + }, + "request.WebsiteNginxUpdate": { + "type": "object", + "required": [ + "content", + "id" + ], + "properties": { + "content": { + "type": "string" + }, + "id": { + "type": "integer" + } + } + }, + "request.WebsiteOp": { + "type": "object", + "required": [ + "id" + ], + "properties": { + "id": { + "type": "integer" + }, + "operate": { + "type": "string" + } + } + }, + "request.WebsitePHPConfigUpdate": { + "type": "object", + "required": [ + "id", + "scope" + ], + "properties": { + "disableFunctions": { + "type": "array", + "items": { + "type": "string" + } + }, + "id": { + "type": "integer" + }, + "params": { + "type": "object", + "additionalProperties": { + "type": "string" + } + }, + "scope": { + "type": "string" + }, + "uploadMaxSize": { + "type": "string" + } + } + }, + "request.WebsitePHPFileUpdate": { + "type": "object", + "required": [ + "content", + "id", + "type" + ], + "properties": { + "content": { + "type": "string" + }, + "id": { + "type": "integer" + }, + "type": { + "type": "string" + } + } + }, + "request.WebsitePHPVersionReq": { + "type": "object", + "required": [ + "runtimeID", + "websiteID" + ], + "properties": { + "retainConfig": { + "type": "boolean" + }, + "runtimeID": { + "type": "integer" + }, + "websiteID": { + "type": "integer" + } + } + }, + "request.WebsiteProxyConfig": { + "type": "object", + "required": [ + "id", + "match", + "name", + "operate", + "proxyHost", + "proxyPass" + ], + "properties": { + "cache": { + "type": "boolean" + }, + "cacheTime": { + "type": "integer" + }, + "cacheUnit": { + "type": "string" + }, + "content": { + "type": "string" + }, + "enable": { + "type": "boolean" + }, + "filePath": { + "type": "string" + }, + "id": { + "type": "integer" + }, + "match": { + "type": "string" + }, + "modifier": { + "type": "string" + }, + "name": { + "type": "string" + }, + "operate": { + "type": "string" + }, + "proxyHost": { + "type": "string" + }, + "proxyPass": { + "type": "string" + }, + "replaces": { + "type": "object", + "additionalProperties": { + "type": "string" + } + }, + "sni": { + "type": "boolean" + } + } + }, + "request.WebsiteProxyReq": { + "type": "object", + "required": [ + "id" + ], + "properties": { + "id": { + "type": "integer" + } + } + }, + "request.WebsiteResourceReq": { + "type": "object", + "required": [ + "id" + ], + "properties": { + "id": { + "type": "integer" + } + } + }, + "request.WebsiteSSLApply": { + "type": "object", + "required": [ + "ID" + ], + "properties": { + "ID": { + "type": "integer" + }, + "nameservers": { + "type": "array", + "items": { + "type": "string" + } + }, + "skipDNSCheck": { + "type": "boolean" + } + } + }, + "request.WebsiteSSLCreate": { + "type": "object", + "required": [ + "acmeAccountId", + "primaryDomain", + "provider" + ], + "properties": { + "acmeAccountId": { + "type": "integer" + }, + "apply": { + "type": "boolean" + }, + "autoRenew": { + "type": "boolean" + }, + "description": { + "type": "string" + }, + "dir": { + "type": "string" + }, + "disableCNAME": { + "type": "boolean" + }, + "dnsAccountId": { + "type": "integer" + }, + "execShell": { + "type": "boolean" + }, + "id": { + "type": "integer" + }, + "keyType": { + "type": "string" + }, + "nameserver1": { + "type": "string" + }, + "nameserver2": { + "type": "string" + }, + "otherDomains": { + "type": "string" + }, + "primaryDomain": { + "type": "string" + }, + "provider": { + "type": "string" + }, + "pushDir": { + "type": "boolean" + }, + "shell": { + "type": "string" + }, + "skipDNS": { + "type": "boolean" + } + } + }, + "request.WebsiteSSLSearch": { + "type": "object", + "required": [ + "page", + "pageSize" + ], + "properties": { + "acmeAccountID": { + "type": "string" + }, + "page": { + "type": "integer" + }, + "pageSize": { + "type": "integer" + } + } + }, + "request.WebsiteSSLUpdate": { + "type": "object", + "required": [ + "id", + "primaryDomain", + "provider" + ], + "properties": { + "acmeAccountId": { + "type": "integer" + }, + "apply": { + "type": "boolean" + }, + "autoRenew": { + "type": "boolean" + }, + "description": { + "type": "string" + }, + "dir": { + "type": "string" + }, + "disableCNAME": { + "type": "boolean" + }, + "dnsAccountId": { + "type": "integer" + }, + "execShell": { + "type": "boolean" + }, + "id": { + "type": "integer" + }, + "keyType": { + "type": "string" + }, + "nameserver1": { + "type": "string" + }, + "nameserver2": { + "type": "string" + }, + "otherDomains": { + "type": "string" + }, + "primaryDomain": { + "type": "string" + }, + "provider": { + "type": "string" + }, + "pushDir": { + "type": "boolean" + }, + "shell": { + "type": "string" + }, + "skipDNS": { + "type": "boolean" + } + } + }, + "request.WebsiteSSLUpload": { + "type": "object", + "required": [ + "type" + ], + "properties": { + "certificate": { + "type": "string" + }, + "certificatePath": { + "type": "string" + }, + "description": { + "type": "string" + }, + "privateKey": { + "type": "string" + }, + "privateKeyPath": { + "type": "string" + }, + "sslID": { + "type": "integer" + }, + "type": { + "type": "string", + "enum": [ + "paste", + "local" + ] + } + } + }, + "request.WebsiteSearch": { + "type": "object", + "required": [ + "order", + "orderBy", + "page", + "pageSize" + ], + "properties": { + "name": { + "type": "string" + }, + "order": { + "type": "string", + "enum": [ + "null", + "ascending", + "descending" + ] + }, + "orderBy": { + "type": "string", + "enum": [ + "primary_domain", + "type", + "status", + "created_at", + "expire_date" + ] + }, + "page": { + "type": "integer" + }, + "pageSize": { + "type": "integer" + }, + "websiteGroupId": { + "type": "integer" + } + } + }, + "request.WebsiteUpdate": { + "type": "object", + "required": [ + "id", + "primaryDomain" + ], + "properties": { + "IPV6": { + "type": "boolean" + }, + "expireDate": { + "type": "string" + }, + "id": { + "type": "integer" + }, + "primaryDomain": { + "type": "string" + }, + "remark": { + "type": "string" + }, + "webSiteGroupID": { + "type": "integer" + } + } + }, + "request.WebsiteUpdateDir": { + "type": "object", + "required": [ + "id", + "siteDir" + ], + "properties": { + "id": { + "type": "integer" + }, + "siteDir": { + "type": "string" + } + } + }, + "request.WebsiteUpdateDirPermission": { + "type": "object", + "required": [ + "group", + "id", + "user" + ], + "properties": { + "group": { + "type": "string" + }, + "id": { + "type": "integer" + }, + "user": { + "type": "string" + } + } + }, + "response.AppDTO": { + "type": "object", + "properties": { + "createdAt": { + "type": "string" + }, + "crossVersionUpdate": { + "type": "boolean" + }, + "document": { + "type": "string" + }, + "github": { + "type": "string" + }, + "icon": { + "type": "string" + }, + "id": { + "type": "integer" + }, + "installed": { + "type": "boolean" + }, + "key": { + "type": "string" + }, + "lastModified": { + "type": "integer" + }, + "limit": { + "type": "integer" + }, + "name": { + "type": "string" + }, + "readMe": { + "type": "string" + }, + "recommend": { + "type": "integer" + }, + "required": { + "type": "string" + }, + "resource": { + "type": "string" + }, + "shortDescEn": { + "type": "string" + }, + "shortDescZh": { + "type": "string" + }, + "status": { + "type": "string" + }, + "tags": { + "type": "array", + "items": { + "$ref": "#/definitions/model.Tag" + } + }, + "type": { + "type": "string" + }, + "updatedAt": { + "type": "string" + }, + "versions": { + "type": "array", + "items": { + "type": "string" + } + }, + "website": { + "type": "string" + } + } + }, + "response.AppDetailDTO": { + "type": "object", + "properties": { + "appId": { + "type": "integer" + }, + "createdAt": { + "type": "string" + }, + "dockerCompose": { + "type": "string" + }, + "downloadCallBackUrl": { + "type": "string" + }, + "downloadUrl": { + "type": "string" + }, + "enable": { + "type": "boolean" + }, + "hostMode": { + "type": "boolean" + }, + "id": { + "type": "integer" + }, + "ignoreUpgrade": { + "type": "boolean" + }, + "image": { + "type": "string" + }, + "lastModified": { + "type": "integer" + }, + "lastVersion": { + "type": "string" + }, + "params": {}, + "status": { + "type": "string" + }, + "update": { + "type": "boolean" + }, + "updatedAt": { + "type": "string" + }, + "version": { + "type": "string" + } + } + }, + "response.AppInstalledCheck": { + "type": "object", + "properties": { + "app": { + "type": "string" + }, + "appInstallId": { + "type": "integer" + }, + "containerName": { + "type": "string" + }, + "createdAt": { + "type": "string" + }, + "httpPort": { + "type": "integer" + }, + "httpsPort": { + "type": "integer" + }, + "installPath": { + "type": "string" + }, + "isExist": { + "type": "boolean" + }, + "lastBackupAt": { + "type": "string" + }, + "name": { + "type": "string" + }, + "status": { + "type": "string" + }, + "version": { + "type": "string" + } + } + }, + "response.AppParam": { + "type": "object", + "properties": { + "edit": { + "type": "boolean" + }, + "key": { + "type": "string" + }, + "labelEn": { + "type": "string" + }, + "labelZh": { + "type": "string" + }, + "multiple": { + "type": "boolean" + }, + "required": { + "type": "boolean" + }, + "rule": { + "type": "string" + }, + "showValue": { + "type": "string" + }, + "type": { + "type": "string" + }, + "value": {}, + "values": {} + } + }, + "response.AppService": { + "type": "object", + "properties": { + "config": {}, + "from": { + "type": "string" + }, + "label": { + "type": "string" + }, + "value": { + "type": "string" + } + } + }, + "response.FileInfo": { + "type": "object", + "properties": { + "content": { + "type": "string" + }, + "extension": { + "type": "string" + }, + "favoriteID": { + "type": "integer" + }, + "gid": { + "type": "string" + }, + "group": { + "type": "string" + }, + "isDetail": { + "type": "boolean" + }, + "isDir": { + "type": "boolean" + }, + "isHidden": { + "type": "boolean" + }, + "isSymlink": { + "type": "boolean" + }, + "itemTotal": { + "type": "integer" + }, + "items": { + "type": "array", + "items": { + "$ref": "#/definitions/files.FileInfo" + } + }, + "linkPath": { + "type": "string" + }, + "mimeType": { + "type": "string" + }, + "modTime": { + "type": "string" + }, + "mode": { + "type": "string" + }, + "name": { + "type": "string" + }, + "path": { + "type": "string" + }, + "size": { + "type": "integer" + }, + "type": { + "type": "string" + }, + "uid": { + "type": "string" + }, + "updateTime": { + "type": "string" + }, + "user": { + "type": "string" + } + } + }, + "response.FileTree": { + "type": "object", + "properties": { + "children": { + "type": "array", + "items": { + "$ref": "#/definitions/response.FileTree" + } + }, + "extension": { + "type": "string" + }, + "id": { + "type": "string" + }, + "isDir": { + "type": "boolean" + }, + "name": { + "type": "string" + }, + "path": { + "type": "string" + } + } + }, + "response.IgnoredApp": { + "type": "object", + "properties": { + "detailID": { + "type": "integer" + }, + "icon": { + "type": "string" + }, + "name": { + "type": "string" + }, + "version": { + "type": "string" + } + } + }, + "response.NginxParam": { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "params": { + "type": "array", + "items": { + "type": "string" + } + } + } + }, + "response.NginxStatus": { + "type": "object", + "properties": { + "accepts": { + "type": "string" + }, + "active": { + "type": "string" + }, + "handled": { + "type": "string" + }, + "reading": { + "type": "string" + }, + "requests": { + "type": "string" + }, + "waiting": { + "type": "string" + }, + "writing": { + "type": "string" + } + } + }, + "response.PHPConfig": { + "type": "object", + "properties": { + "disableFunctions": { + "type": "array", + "items": { + "type": "string" + } + }, + "params": { + "type": "object", + "additionalProperties": { + "type": "string" + } + }, + "uploadMaxSize": { + "type": "string" + } + } + }, + "response.PHPExtensionsDTO": { + "type": "object", + "properties": { + "createdAt": { + "type": "string" + }, + "extensions": { + "type": "string" + }, + "id": { + "type": "integer" + }, + "name": { + "type": "string" + }, + "updatedAt": { + "type": "string" + } + } + }, + "response.WebsiteAcmeAccountDTO": { + "type": "object", + "properties": { + "createdAt": { + "type": "string" + }, + "eabHmacKey": { + "type": "string" + }, + "eabKid": { + "type": "string" + }, + "email": { + "type": "string" + }, + "id": { + "type": "integer" + }, + "keyType": { + "type": "string" + }, + "type": { + "type": "string" + }, + "updatedAt": { + "type": "string" + }, + "url": { + "type": "string" + } + } + }, + "response.WebsiteCADTO": { + "type": "object", + "properties": { + "city": { + "type": "string" + }, + "commonName": { + "type": "string" + }, + "country": { + "type": "string" + }, + "createdAt": { + "type": "string" + }, + "csr": { + "type": "string" + }, + "id": { + "type": "integer" + }, + "keyType": { + "type": "string" + }, + "name": { + "type": "string" + }, + "organization": { + "type": "string" + }, + "organizationUint": { + "type": "string" + }, + "privateKey": { + "type": "string" + }, + "province": { + "type": "string" + }, + "updatedAt": { + "type": "string" + } + } + }, + "response.WebsiteDNSRes": { + "type": "object", + "properties": { + "domain": { + "type": "string" + }, + "err": { + "type": "string" + }, + "resolve": { + "type": "string" + }, + "value": { + "type": "string" + } + } + }, + "response.WebsiteDTO": { + "type": "object", + "properties": { + "IPV6": { + "type": "boolean" + }, + "accessLog": { + "type": "boolean" + }, + "accessLogPath": { + "type": "string" + }, + "alias": { + "type": "string" + }, + "appInstallId": { + "type": "integer" + }, + "appName": { + "type": "string" + }, + "createdAt": { + "type": "string" + }, + "defaultServer": { + "type": "boolean" + }, + "domains": { + "type": "array", + "items": { + "$ref": "#/definitions/model.WebsiteDomain" + } + }, + "errorLog": { + "type": "boolean" + }, + "errorLogPath": { + "type": "string" + }, + "expireDate": { + "type": "string" + }, + "ftpId": { + "type": "integer" + }, + "group": { + "type": "string" + }, + "httpConfig": { + "type": "string" + }, + "id": { + "type": "integer" + }, + "primaryDomain": { + "type": "string" + }, + "protocol": { + "type": "string" + }, + "proxy": { + "type": "string" + }, + "proxyType": { + "type": "string" + }, + "remark": { + "type": "string" + }, + "rewrite": { + "type": "string" + }, + "runtimeID": { + "type": "integer" + }, + "runtimeName": { + "type": "string" + }, + "siteDir": { + "type": "string" + }, + "sitePath": { + "type": "string" + }, + "status": { + "type": "string" + }, + "type": { + "type": "string" + }, + "updatedAt": { + "type": "string" + }, + "user": { + "type": "string" + }, + "webSiteGroupId": { + "type": "integer" + }, + "webSiteSSL": { + "$ref": "#/definitions/model.WebsiteSSL" + }, + "webSiteSSLId": { + "type": "integer" + } + } + }, + "response.WebsiteHTTPS": { + "type": "object", + "properties": { + "SSL": { + "$ref": "#/definitions/model.WebsiteSSL" + }, + "SSLProtocol": { + "type": "array", + "items": { + "type": "string" + } + }, + "algorithm": { + "type": "string" + }, + "enable": { + "type": "boolean" + }, + "hsts": { + "type": "boolean" + }, + "httpConfig": { + "type": "string" + } + } + }, + "response.WebsiteLog": { + "type": "object", + "properties": { + "content": { + "type": "string" + }, + "enable": { + "type": "boolean" + }, + "end": { + "type": "boolean" + }, + "path": { + "type": "string" + } + } + }, + "response.WebsiteNginxConfig": { + "type": "object", + "properties": { + "enable": { + "type": "boolean" + }, + "params": { + "type": "array", + "items": { + "$ref": "#/definitions/response.NginxParam" + } + } + } + }, + "response.WebsitePreInstallCheck": { + "type": "object", + "properties": { + "appName": { + "type": "string" + }, + "name": { + "type": "string" + }, + "status": { + "type": "string" + }, + "version": { + "type": "string" + } + } + } + } +}` + +// SwaggerInfo holds exported Swagger Info so clients can modify it +var SwaggerInfo = &swag.Spec{ + Version: "1.0", + Host: "localhost", + BasePath: "/api/v1", + Schemes: []string{}, + Title: "1Panel", + Description: "开源Linux面板", + InfoInstanceName: "swagger", + SwaggerTemplate: docTemplate, +} + +func init() { + swag.Register(SwaggerInfo.InstanceName(), SwaggerInfo) +} diff --git a/agent/cmd/server/docs/swagger.go b/agent/cmd/server/docs/swagger.go new file mode 100644 index 000000000..b0022e43c --- /dev/null +++ b/agent/cmd/server/docs/swagger.go @@ -0,0 +1,6 @@ +package docs + +import _ "embed" + +//go:embed swagger.json +var SwaggerJson []byte diff --git a/agent/cmd/server/docs/swagger.json b/agent/cmd/server/docs/swagger.json new file mode 100644 index 000000000..de2b02277 --- /dev/null +++ b/agent/cmd/server/docs/swagger.json @@ -0,0 +1,23441 @@ +{ + "swagger": "2.0", + "info": { + "description": "开源Linux面板", + "title": "1Panel", + "termsOfService": "http://swagger.io/terms/", + "contact": {}, + "license": { + "name": "Apache 2.0", + "url": "http://www.apache.org/licenses/LICENSE-2.0.html" + }, + "version": "1.0" + }, + "host": "localhost", + "basePath": "/api/v1", + "paths": { + "/apps/:key": { + "get": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "通过 key 获取应用信息", + "consumes": [ + "application/json" + ], + "tags": [ + "App" + ], + "summary": "Search app by key", + "parameters": [ + { + "type": "string", + "description": "app key", + "name": "key", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/response.AppDTO" + } + } + } + } + }, + "/apps/checkupdate": { + "get": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "获取应用更新版本", + "tags": [ + "App" + ], + "summary": "Get app list update", + "responses": { + "200": { + "description": "OK" + } + } + } + }, + "/apps/detail/:appId/:version/:type": { + "get": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "通过 appid 获取应用详情", + "consumes": [ + "application/json" + ], + "tags": [ + "App" + ], + "summary": "Search app detail by appid", + "parameters": [ + { + "type": "integer", + "description": "app id", + "name": "appId", + "in": "path", + "required": true + }, + { + "type": "string", + "description": "app 版本", + "name": "version", + "in": "path", + "required": true + }, + { + "type": "string", + "description": "app 类型", + "name": "version", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/response.AppDetailDTO" + } + } + } + } + }, + "/apps/details/:id": { + "get": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "通过 id 获取应用详情", + "consumes": [ + "application/json" + ], + "tags": [ + "App" + ], + "summary": "Get app detail by id", + "parameters": [ + { + "type": "integer", + "description": "id", + "name": "appId", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/response.AppDetailDTO" + } + } + } + } + }, + "/apps/ignored": { + "get": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "获取忽略的应用版本", + "consumes": [ + "application/json" + ], + "tags": [ + "App" + ], + "summary": "Get Ignore App", + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/response.IgnoredApp" + } + } + } + } + }, + "/apps/install": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "安装应用", + "consumes": [ + "application/json" + ], + "tags": [ + "App" + ], + "summary": "Install app", + "parameters": [ + { + "description": "request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/request.AppInstallCreate" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/model.AppInstall" + } + } + }, + "x-panel-log": { + "BeforeFunctions": [ + { + "db": "app_installs", + "input_column": "name", + "input_value": "name", + "isList": false, + "output_column": "app_id", + "output_value": "appId" + }, + { + "db": "apps", + "info": "appId", + "isList": false, + "output_column": "key", + "output_value": "appKey" + } + ], + "bodyKeys": [ + "name" + ], + "formatEN": "Install app [appKey]-[name]", + "formatZH": "安装应用 [appKey]-[name]", + "paramKeys": [] + } + } + }, + "/apps/installed/check": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "检查应用安装情况", + "consumes": [ + "application/json" + ], + "tags": [ + "App" + ], + "summary": "Check app installed", + "parameters": [ + { + "description": "request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/request.AppInstalledInfo" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/response.AppInstalledCheck" + } + } + } + } + }, + "/apps/installed/conf": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "通过 key 获取应用默认配置", + "consumes": [ + "application/json" + ], + "tags": [ + "App" + ], + "summary": "Search default config by key", + "parameters": [ + { + "description": "request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.OperationWithNameAndType" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "string" + } + } + } + } + }, + "/apps/installed/conninfo/:key": { + "get": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "获取应用连接信息", + "consumes": [ + "application/json" + ], + "tags": [ + "App" + ], + "summary": "Search app password by key", + "parameters": [ + { + "description": "request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.OperationWithNameAndType" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "string" + } + } + } + } + }, + "/apps/installed/delete/check/:appInstallId": { + "get": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "删除前检查", + "consumes": [ + "application/json" + ], + "tags": [ + "App" + ], + "summary": "Check before delete", + "parameters": [ + { + "type": "integer", + "description": "App install id", + "name": "appInstallId", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/dto.AppResource" + } + } + } + } + } + }, + "/apps/installed/ignore": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "忽略应用升级版本", + "consumes": [ + "application/json" + ], + "tags": [ + "App" + ], + "summary": "ignore App Update", + "parameters": [ + { + "description": "request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/request.AppInstalledIgnoreUpgrade" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + }, + "x-panel-log": { + "BeforeFunctions": [], + "bodyKeys": [ + "installId" + ], + "formatEN": "Application param update [installId]", + "formatZH": "忽略应用 [installId] 版本升级", + "paramKeys": [] + } + } + }, + "/apps/installed/list": { + "get": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "获取已安装应用列表", + "consumes": [ + "application/json" + ], + "tags": [ + "App" + ], + "summary": "List app installed", + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/dto.AppInstallInfo" + } + } + } + } + } + }, + "/apps/installed/loadport": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "获取应用端口", + "consumes": [ + "application/json" + ], + "tags": [ + "App" + ], + "summary": "Search app port by key", + "parameters": [ + { + "description": "request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.OperationWithNameAndType" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "integer" + } + } + } + } + }, + "/apps/installed/op": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "操作已安装应用", + "consumes": [ + "application/json" + ], + "tags": [ + "App" + ], + "summary": "Operate installed app", + "parameters": [ + { + "description": "request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/request.AppInstalledOperate" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + }, + "x-panel-log": { + "BeforeFunctions": [ + { + "db": "app_installs", + "input_column": "id", + "input_value": "installId", + "isList": false, + "output_column": "app_id", + "output_value": "appId" + }, + { + "db": "app_installs", + "input_column": "id", + "input_value": "installId", + "isList": false, + "output_column": "name", + "output_value": "appName" + }, + { + "db": "apps", + "input_column": "id", + "input_value": "appId", + "isList": false, + "output_column": "key", + "output_value": "appKey" + } + ], + "bodyKeys": [ + "installId", + "operate" + ], + "formatEN": "[operate] App [appKey][appName]", + "formatZH": "[operate] 应用 [appKey][appName]", + "paramKeys": [] + } + } + }, + "/apps/installed/params/:appInstallId": { + "get": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "通过 install id 获取应用参数", + "consumes": [ + "application/json" + ], + "tags": [ + "App" + ], + "summary": "Search params by appInstallId", + "parameters": [ + { + "type": "string", + "description": "request", + "name": "appInstallId", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/response.AppParam" + } + } + } + } + }, + "/apps/installed/params/update": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "修改应用参数", + "consumes": [ + "application/json" + ], + "tags": [ + "App" + ], + "summary": "Change app params", + "parameters": [ + { + "description": "request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/request.AppInstalledUpdate" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + }, + "x-panel-log": { + "BeforeFunctions": [], + "bodyKeys": [ + "installId" + ], + "formatEN": "Application param update [installId]", + "formatZH": "应用参数修改 [installId]", + "paramKeys": [] + } + } + }, + "/apps/installed/port/change": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "修改应用端口", + "consumes": [ + "application/json" + ], + "tags": [ + "App" + ], + "summary": "Change app port", + "parameters": [ + { + "description": "request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/request.PortUpdate" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + }, + "x-panel-log": { + "BeforeFunctions": [], + "bodyKeys": [ + "key", + "name", + "port" + ], + "formatEN": "Application port update [key]-[name] =\u003e [port]", + "formatZH": "应用端口修改 [key]-[name] =\u003e [port]", + "paramKeys": [] + } + } + }, + "/apps/installed/search": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "分页获取已安装应用列表", + "consumes": [ + "application/json" + ], + "tags": [ + "App" + ], + "summary": "Page app installed", + "parameters": [ + { + "description": "request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/request.AppInstalledSearch" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + } + } + }, + "/apps/installed/sync": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "同步已安装应用列表", + "tags": [ + "App" + ], + "summary": "Sync app installed", + "responses": { + "200": { + "description": "OK" + } + }, + "x-panel-log": { + "BeforeFunctions": [], + "bodyKeys": [], + "formatEN": "Sync the list of installed apps", + "formatZH": "同步已安装应用列表", + "paramKeys": [] + } + } + }, + "/apps/installed/update/versions": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "通过 install id 获取应用更新版本", + "consumes": [ + "application/json" + ], + "tags": [ + "App" + ], + "summary": "Search app update version by install id", + "parameters": [ + { + "type": "integer", + "description": "request", + "name": "appInstallId", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/dto.AppVersion" + } + } + } + } + } + }, + "/apps/search": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "获取应用列表", + "consumes": [ + "application/json" + ], + "tags": [ + "App" + ], + "summary": "List apps", + "parameters": [ + { + "description": "request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/request.AppSearch" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + } + } + }, + "/apps/services/:key": { + "get": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "通过 key 获取应用 service", + "consumes": [ + "application/json" + ], + "tags": [ + "App" + ], + "summary": "Search app service by key", + "parameters": [ + { + "type": "string", + "description": "request", + "name": "key", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/response.AppService" + } + } + } + } + } + }, + "/apps/sync": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "同步应用列表", + "tags": [ + "App" + ], + "summary": "Sync app list", + "responses": { + "200": { + "description": "OK" + } + }, + "x-panel-log": { + "BeforeFunctions": [], + "bodyKeys": [], + "formatEN": "App store synchronization", + "formatZH": "应用商店同步", + "paramKeys": [] + } + } + }, + "/auth/captcha": { + "get": { + "description": "加载验证码", + "tags": [ + "Auth" + ], + "summary": "Load captcha", + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/dto.CaptchaResponse" + } + } + } + } + }, + "/auth/demo": { + "get": { + "description": "判断是否为demo环境", + "tags": [ + "Auth" + ], + "summary": "Check System isDemo", + "responses": { + "200": { + "description": "OK" + } + } + } + }, + "/auth/issafety": { + "get": { + "description": "获取系统安全登录状态", + "tags": [ + "Auth" + ], + "summary": "Load safety status", + "responses": { + "200": { + "description": "OK" + } + } + } + }, + "/auth/language": { + "get": { + "description": "获取系统语言设置", + "tags": [ + "Auth" + ], + "summary": "Load System Language", + "responses": { + "200": { + "description": "OK" + } + } + } + }, + "/auth/login": { + "post": { + "description": "用户登录", + "consumes": [ + "application/json" + ], + "tags": [ + "Auth" + ], + "summary": "User login", + "parameters": [ + { + "type": "string", + "description": "安全入口 base64 加密串", + "name": "EntranceCode", + "in": "header", + "required": true + }, + { + "description": "request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.Login" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/dto.UserLoginInfo" + } + } + } + } + }, + "/auth/logout": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "用户登出", + "tags": [ + "Auth" + ], + "summary": "User logout", + "responses": { + "200": { + "description": "OK" + } + } + } + }, + "/auth/mfalogin": { + "post": { + "description": "用户 mfa 登录", + "consumes": [ + "application/json" + ], + "tags": [ + "Auth" + ], + "summary": "User login with mfa", + "parameters": [ + { + "description": "request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.MFALogin" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/dto.UserLoginInfo" + }, + "headers": { + "EntranceCode": { + "type": "string", + "description": "安全入口" + } + } + } + } + } + }, + "/containers": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "创建容器", + "consumes": [ + "application/json" + ], + "tags": [ + "Container" + ], + "summary": "Create container", + "parameters": [ + { + "description": "request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.ContainerOperate" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + }, + "x-panel-log": { + "BeforeFunctions": [], + "bodyKeys": [ + "name", + "image" + ], + "formatEN": "create container [name][image]", + "formatZH": "创建容器 [name][image]", + "paramKeys": [] + } + } + }, + "/containers/clean/log": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "清理容器日志", + "consumes": [ + "application/json" + ], + "tags": [ + "Container" + ], + "summary": "Clean container log", + "parameters": [ + { + "description": "request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.OperationWithName" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + }, + "x-panel-log": { + "BeforeFunctions": [], + "bodyKeys": [ + "name" + ], + "formatEN": "clean container [name] logs", + "formatZH": "清理容器 [name] 日志", + "paramKeys": [] + } + } + }, + "/containers/commit": { + "post": { + "description": "容器提交生成新镜像", + "consumes": [ + "application/json" + ], + "tags": [ + "Container" + ], + "summary": "Commit Container", + "parameters": [ + { + "description": "request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.ContainerCommit" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + } + } + }, + "/containers/compose": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "创建容器编排", + "consumes": [ + "application/json" + ], + "tags": [ + "Container Compose" + ], + "summary": "Create compose", + "parameters": [ + { + "description": "request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.ComposeCreate" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + }, + "x-panel-log": { + "BeforeFunctions": [], + "bodyKeys": [ + "name" + ], + "formatEN": "create compose [name]", + "formatZH": "创建 compose [name]", + "paramKeys": [] + } + } + }, + "/containers/compose/operate": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "容器编排操作", + "consumes": [ + "application/json" + ], + "tags": [ + "Container Compose" + ], + "summary": "Operate compose", + "parameters": [ + { + "description": "request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.ComposeOperation" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + }, + "x-panel-log": { + "BeforeFunctions": [], + "bodyKeys": [ + "name", + "operation" + ], + "formatEN": "compose [operation] [name]", + "formatZH": "compose [operation] [name]", + "paramKeys": [] + } + } + }, + "/containers/compose/search": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "获取编排列表分页", + "consumes": [ + "application/json" + ], + "tags": [ + "Container Compose" + ], + "summary": "Page composes", + "parameters": [ + { + "description": "request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.SearchWithPage" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/dto.PageResult" + } + } + } + } + }, + "/containers/compose/search/log": { + "get": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "docker-compose 日志", + "tags": [ + "Container Compose" + ], + "summary": "Container Compose logs", + "parameters": [ + { + "type": "string", + "description": "compose 文件地址", + "name": "compose", + "in": "query" + }, + { + "type": "string", + "description": "时间筛选", + "name": "since", + "in": "query" + }, + { + "type": "string", + "description": "是否追踪", + "name": "follow", + "in": "query" + }, + { + "type": "string", + "description": "显示行号", + "name": "tail", + "in": "query" + } + ], + "responses": {} + } + }, + "/containers/compose/test": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "测试 compose 是否可用", + "consumes": [ + "application/json" + ], + "tags": [ + "Container Compose" + ], + "summary": "Test compose", + "parameters": [ + { + "description": "request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.ComposeCreate" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + }, + "x-panel-log": { + "BeforeFunctions": [], + "bodyKeys": [ + "name" + ], + "formatEN": "check compose [name]", + "formatZH": "检测 compose [name] 格式", + "paramKeys": [] + } + } + }, + "/containers/compose/update": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "更新容器编排", + "consumes": [ + "application/json" + ], + "tags": [ + "Container Compose" + ], + "summary": "Update compose", + "parameters": [ + { + "description": "request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.ComposeUpdate" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + }, + "x-panel-log": { + "BeforeFunctions": [], + "bodyKeys": [ + "name" + ], + "formatEN": "update compose information [name]", + "formatZH": "更新 compose [name]", + "paramKeys": [] + } + } + }, + "/containers/daemonjson": { + "get": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "获取 docker 配置信息", + "produces": [ + "application/json" + ], + "tags": [ + "Container Docker" + ], + "summary": "Load docker daemon.json", + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/dto.DaemonJsonConf" + } + } + } + } + }, + "/containers/daemonjson/file": { + "get": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "获取 docker 配置信息(表单)", + "produces": [ + "application/json" + ], + "tags": [ + "Container Docker" + ], + "summary": "Load docker daemon.json", + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "string" + } + } + } + } + }, + "/containers/daemonjson/update": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "修改 docker 配置信息", + "consumes": [ + "application/json" + ], + "tags": [ + "Container Docker" + ], + "summary": "Update docker daemon.json", + "parameters": [ + { + "description": "request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.SettingUpdate" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + }, + "x-panel-log": { + "BeforeFunctions": [], + "bodyKeys": [ + "key", + "value" + ], + "formatEN": "Updated configuration [key]", + "formatZH": "更新配置 [key]", + "paramKeys": [] + } + } + }, + "/containers/daemonjson/update/byfile": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "上传替换 docker 配置文件", + "consumes": [ + "application/json" + ], + "tags": [ + "Container Docker" + ], + "summary": "Update docker daemon.json by upload file", + "parameters": [ + { + "description": "request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.DaemonJsonUpdateByFile" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + }, + "x-panel-log": { + "BeforeFunctions": [], + "bodyKeys": [], + "formatEN": "Updated configuration file", + "formatZH": "更新配置文件", + "paramKeys": [] + } + } + }, + "/containers/docker/operate": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "Docker 操作", + "consumes": [ + "application/json" + ], + "tags": [ + "Container Docker" + ], + "summary": "Operate docker", + "parameters": [ + { + "description": "request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.DockerOperation" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + }, + "x-panel-log": { + "BeforeFunctions": [], + "bodyKeys": [ + "operation" + ], + "formatEN": "[operation] docker service", + "formatZH": "docker 服务 [operation]", + "paramKeys": [] + } + } + }, + "/containers/docker/status": { + "get": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "获取 docker 服务状态", + "produces": [ + "application/json" + ], + "tags": [ + "Container Docker" + ], + "summary": "Load docker status", + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "string" + } + } + } + } + }, + "/containers/download/log": { + "post": { + "description": "下载容器日志", + "responses": {} + } + }, + "/containers/image": { + "get": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "获取镜像名称列表", + "produces": [ + "application/json" + ], + "tags": [ + "Container Image" + ], + "summary": "load images options", + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/dto.Options" + } + } + } + } + } + }, + "/containers/image/all": { + "get": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "获取所有镜像列表", + "produces": [ + "application/json" + ], + "tags": [ + "Container Image" + ], + "summary": "List all images", + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/dto.ImageInfo" + } + } + } + } + } + }, + "/containers/image/build": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "构建镜像", + "consumes": [ + "application/json" + ], + "tags": [ + "Container Image" + ], + "summary": "Build image", + "parameters": [ + { + "description": "request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.ImageBuild" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "string" + } + } + }, + "x-panel-log": { + "BeforeFunctions": [], + "bodyKeys": [ + "name" + ], + "formatEN": "build image [name]", + "formatZH": "构建镜像 [name]", + "paramKeys": [] + } + } + }, + "/containers/image/load": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "导入镜像", + "consumes": [ + "application/json" + ], + "tags": [ + "Container Image" + ], + "summary": "Load image", + "parameters": [ + { + "description": "request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.ImageLoad" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + }, + "x-panel-log": { + "BeforeFunctions": [], + "bodyKeys": [ + "path" + ], + "formatEN": "load image from [path]", + "formatZH": "从 [path] 加载镜像", + "paramKeys": [] + } + } + }, + "/containers/image/pull": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "拉取镜像", + "consumes": [ + "application/json" + ], + "tags": [ + "Container Image" + ], + "summary": "Pull image", + "parameters": [ + { + "description": "request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.ImagePull" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "string" + } + } + }, + "x-panel-log": { + "BeforeFunctions": [ + { + "db": "image_repos", + "input_column": "id", + "input_value": "repoID", + "isList": false, + "output_column": "name", + "output_value": "reponame" + } + ], + "bodyKeys": [ + "repoID", + "imageName" + ], + "formatEN": "image pull [reponame][imageName]", + "formatZH": "镜像拉取 [reponame][imageName]", + "paramKeys": [] + } + } + }, + "/containers/image/push": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "推送镜像", + "consumes": [ + "application/json" + ], + "tags": [ + "Container Image" + ], + "summary": "Push image", + "parameters": [ + { + "description": "request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.ImagePush" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "string" + } + } + }, + "x-panel-log": { + "BeforeFunctions": [ + { + "db": "image_repos", + "input_column": "id", + "input_value": "repoID", + "isList": false, + "output_column": "name", + "output_value": "reponame" + } + ], + "bodyKeys": [ + "repoID", + "tagName", + "name" + ], + "formatEN": "push [tagName] to [reponame][name]", + "formatZH": "[tagName] 推送到 [reponame][name]", + "paramKeys": [] + } + } + }, + "/containers/image/remove": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "删除镜像", + "consumes": [ + "application/json" + ], + "tags": [ + "Container Image" + ], + "summary": "Delete image", + "parameters": [ + { + "description": "request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.BatchDelete" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + }, + "x-panel-log": { + "BeforeFunctions": [], + "bodyKeys": [ + "names" + ], + "formatEN": "remove image [names]", + "formatZH": "移除镜像 [names]", + "paramKeys": [] + } + } + }, + "/containers/image/save": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "导出镜像", + "consumes": [ + "application/json" + ], + "tags": [ + "Container Image" + ], + "summary": "Save image", + "parameters": [ + { + "description": "request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.ImageSave" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + }, + "x-panel-log": { + "BeforeFunctions": [], + "bodyKeys": [ + "tagName", + "path", + "name" + ], + "formatEN": "save [tagName] as [path]/[name]", + "formatZH": "保留 [tagName] 为 [path]/[name]", + "paramKeys": [] + } + } + }, + "/containers/image/search": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "获取镜像列表分页", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Container Image" + ], + "summary": "Page images", + "parameters": [ + { + "description": "request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.SearchWithPage" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/dto.PageResult" + } + } + } + } + }, + "/containers/image/tag": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "Tag 镜像", + "consumes": [ + "application/json" + ], + "tags": [ + "Container Image" + ], + "summary": "Tag image", + "parameters": [ + { + "description": "request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.ImageTag" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + }, + "x-panel-log": { + "BeforeFunctions": [ + { + "db": "image_repos", + "input_column": "id", + "input_value": "repoID", + "isList": false, + "output_column": "name", + "output_value": "reponame" + } + ], + "bodyKeys": [ + "repoID", + "targetName" + ], + "formatEN": "tag image [reponame][targetName]", + "formatZH": "tag 镜像 [reponame][targetName]", + "paramKeys": [] + } + } + }, + "/containers/info": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "获取容器表单信息", + "consumes": [ + "application/json" + ], + "tags": [ + "Container" + ], + "summary": "Load container info", + "parameters": [ + { + "description": "request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.OperationWithName" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/dto.ContainerOperate" + } + } + } + } + }, + "/containers/inspect": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "容器详情", + "consumes": [ + "application/json" + ], + "tags": [ + "Container" + ], + "summary": "Container inspect", + "parameters": [ + { + "description": "request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.InspectReq" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "string" + } + } + } + } + }, + "/containers/ipv6option/update": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "修改 docker ipv6 配置", + "consumes": [ + "application/json" + ], + "tags": [ + "Container Docker" + ], + "summary": "Update docker daemon.json ipv6 option", + "parameters": [ + { + "description": "request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.LogOption" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + }, + "x-panel-log": { + "BeforeFunctions": [], + "bodyKeys": [], + "formatEN": "Updated the ipv6 option", + "formatZH": "更新 ipv6 配置", + "paramKeys": [] + } + } + }, + "/containers/limit": { + "get": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "获取容器限制", + "summary": "Load container limits", + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/dto.ResourceLimit" + } + } + } + } + }, + "/containers/list": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "获取容器名称", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Container" + ], + "summary": "List containers", + "responses": { + "200": { + "description": "OK" + } + } + } + }, + "/containers/list/stats": { + "get": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "获取容器列表资源占用", + "summary": "Load container stats", + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/dto.ContainerListStats" + } + } + } + } + } + }, + "/containers/load/log": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "获取容器操作日志", + "consumes": [ + "application/json" + ], + "tags": [ + "Container" + ], + "summary": "Load container log", + "parameters": [ + { + "description": "request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.OperationWithNameAndType" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + } + } + }, + "/containers/logoption/update": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "修改 docker 日志配置", + "consumes": [ + "application/json" + ], + "tags": [ + "Container Docker" + ], + "summary": "Update docker daemon.json log option", + "parameters": [ + { + "description": "request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.LogOption" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + }, + "x-panel-log": { + "BeforeFunctions": [], + "bodyKeys": [], + "formatEN": "Updated the log option", + "formatZH": "更新日志配置", + "paramKeys": [] + } + } + }, + "/containers/network": { + "get": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "获取容器网络列表", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Container Network" + ], + "summary": "List networks", + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/dto.Options" + } + } + } + } + }, + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "创建容器网络", + "consumes": [ + "application/json" + ], + "tags": [ + "Container Network" + ], + "summary": "Create network", + "parameters": [ + { + "description": "request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.NetworkCreate" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + }, + "x-panel-log": { + "BeforeFunctions": [], + "bodyKeys": [ + "name" + ], + "formatEN": "create container network [name]", + "formatZH": "创建容器网络 name", + "paramKeys": [] + } + } + }, + "/containers/network/del": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "删除容器网络", + "consumes": [ + "application/json" + ], + "tags": [ + "Container Network" + ], + "summary": "Delete network", + "parameters": [ + { + "description": "request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.BatchDelete" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + }, + "x-panel-log": { + "BeforeFunctions": [], + "bodyKeys": [ + "names" + ], + "formatEN": "delete container network [names]", + "formatZH": "删除容器网络 [names]", + "paramKeys": [] + } + } + }, + "/containers/network/search": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "获取容器网络列表分页", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Container Network" + ], + "summary": "Page networks", + "parameters": [ + { + "description": "request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.SearchWithPage" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/dto.PageResult" + } + } + } + } + }, + "/containers/operate": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "容器操作", + "consumes": [ + "application/json" + ], + "tags": [ + "Container" + ], + "summary": "Operate Container", + "parameters": [ + { + "description": "request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.ContainerOperation" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + }, + "x-panel-log": { + "BeforeFunctions": [], + "bodyKeys": [ + "names", + "operation" + ], + "formatEN": "container [operation] [names]", + "formatZH": "容器 [names] 执行 [operation]", + "paramKeys": [] + } + } + }, + "/containers/prune": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "容器清理", + "consumes": [ + "application/json" + ], + "tags": [ + "Container" + ], + "summary": "Clean container", + "parameters": [ + { + "description": "request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.ContainerPrune" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/dto.ContainerPruneReport" + } + } + }, + "x-panel-log": { + "BeforeFunctions": [], + "bodyKeys": [ + "pruneType" + ], + "formatEN": "clean container [pruneType]", + "formatZH": "清理容器 [pruneType]", + "paramKeys": [] + } + } + }, + "/containers/rename": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "容器重命名", + "consumes": [ + "application/json" + ], + "tags": [ + "Container" + ], + "summary": "Rename Container", + "parameters": [ + { + "description": "request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.ContainerRename" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + }, + "x-panel-log": { + "BeforeFunctions": [], + "bodyKeys": [ + "name", + "newName" + ], + "formatEN": "rename container [name] =\u003e [newName]", + "formatZH": "容器重命名 [name] =\u003e [newName]", + "paramKeys": [] + } + } + }, + "/containers/repo": { + "get": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "获取镜像仓库列表", + "produces": [ + "application/json" + ], + "tags": [ + "Container Image-repo" + ], + "summary": "List image repos", + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/dto.ImageRepoOption" + } + } + } + } + }, + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "创建镜像仓库", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Container Image-repo" + ], + "summary": "Create image repo", + "parameters": [ + { + "description": "request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.ImageRepoDelete" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + }, + "x-panel-log": { + "BeforeFunctions": [], + "bodyKeys": [ + "name" + ], + "formatEN": "create image repo [name]", + "formatZH": "创建镜像仓库 [name]", + "paramKeys": [] + } + } + }, + "/containers/repo/del": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "删除镜像仓库", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Container Image-repo" + ], + "summary": "Delete image repo", + "parameters": [ + { + "description": "request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.ImageRepoDelete" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + }, + "x-panel-log": { + "BeforeFunctions": [ + { + "db": "image_repos", + "input_column": "id", + "input_value": "ids", + "isList": true, + "output_column": "name", + "output_value": "names" + } + ], + "bodyKeys": [ + "ids" + ], + "formatEN": "delete image repo [names]", + "formatZH": "删除镜像仓库 [names]", + "paramKeys": [] + } + } + }, + "/containers/repo/search": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "获取镜像仓库列表分页", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Container Image-repo" + ], + "summary": "Page image repos", + "parameters": [ + { + "description": "request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.SearchWithPage" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/dto.PageResult" + } + } + } + } + }, + "/containers/repo/status": { + "get": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "获取 docker 仓库状态", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Container Image-repo" + ], + "summary": "Load repo status", + "parameters": [ + { + "description": "request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.OperateByID" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + } + } + }, + "/containers/repo/update": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "更新镜像仓库", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Container Image-repo" + ], + "summary": "Update image repo", + "parameters": [ + { + "description": "request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.ImageRepoUpdate" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + }, + "x-panel-log": { + "BeforeFunctions": [ + { + "db": "image_repos", + "input_column": "id", + "input_value": "id", + "isList": false, + "output_column": "name", + "output_value": "name" + } + ], + "bodyKeys": [ + "id" + ], + "formatEN": "update image repo information [name]", + "formatZH": "更新镜像仓库 [name]", + "paramKeys": [] + } + } + }, + "/containers/search": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "获取容器列表分页", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Container" + ], + "summary": "Page containers", + "parameters": [ + { + "description": "request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.PageContainer" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/dto.PageResult" + } + } + } + } + }, + "/containers/search/log": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "容器日志", + "tags": [ + "Container" + ], + "summary": "Container logs", + "parameters": [ + { + "type": "string", + "description": "容器名称", + "name": "container", + "in": "query" + }, + { + "type": "string", + "description": "时间筛选", + "name": "since", + "in": "query" + }, + { + "type": "string", + "description": "是否追踪", + "name": "follow", + "in": "query" + }, + { + "type": "string", + "description": "显示行号", + "name": "tail", + "in": "query" + } + ], + "responses": {} + } + }, + "/containers/stats/:id": { + "get": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "容器监控信息", + "tags": [ + "Container" + ], + "summary": "Container stats", + "parameters": [ + { + "type": "integer", + "description": "容器id", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/dto.ContainerStats" + } + } + } + } + }, + "/containers/template": { + "get": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "获取容器编排模版列表", + "produces": [ + "application/json" + ], + "tags": [ + "Container Compose-template" + ], + "summary": "List compose templates", + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/dto.ComposeTemplateInfo" + } + } + } + } + }, + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "创建容器编排模版", + "consumes": [ + "application/json" + ], + "tags": [ + "Container Compose-template" + ], + "summary": "Create compose template", + "parameters": [ + { + "description": "request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.ComposeTemplateCreate" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + }, + "x-panel-log": { + "BeforeFunctions": [], + "bodyKeys": [ + "name" + ], + "formatEN": "create compose template [name]", + "formatZH": "创建 compose 模版 [name]", + "paramKeys": [] + } + } + }, + "/containers/template/del": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "删除容器编排模版", + "consumes": [ + "application/json" + ], + "tags": [ + "Container Compose-template" + ], + "summary": "Delete compose template", + "parameters": [ + { + "description": "request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.BatchDelete" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + }, + "x-panel-log": { + "BeforeFunctions": [ + { + "db": "compose_templates", + "input_column": "id", + "input_value": "ids", + "isList": true, + "output_column": "name", + "output_value": "names" + } + ], + "bodyKeys": [ + "ids" + ], + "formatEN": "delete compose template [names]", + "formatZH": "删除 compose 模版 [names]", + "paramKeys": [] + } + } + }, + "/containers/template/search": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "获取容器编排模版列表分页", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Container Compose-template" + ], + "summary": "Page compose templates", + "parameters": [ + { + "description": "request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.SearchWithPage" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/dto.PageResult" + } + } + } + } + }, + "/containers/template/update": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "更新容器编排模版", + "consumes": [ + "application/json" + ], + "tags": [ + "Container Compose-template" + ], + "summary": "Update compose template", + "parameters": [ + { + "description": "request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.ComposeTemplateUpdate" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + }, + "x-panel-log": { + "BeforeFunctions": [ + { + "db": "compose_templates", + "input_column": "id", + "input_value": "id", + "isList": false, + "output_column": "name", + "output_value": "name" + } + ], + "bodyKeys": [ + "id" + ], + "formatEN": "update compose template information [name]", + "formatZH": "更新 compose 模版 [name]", + "paramKeys": [] + } + } + }, + "/containers/update": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "更新容器", + "consumes": [ + "application/json" + ], + "tags": [ + "Container" + ], + "summary": "Update container", + "parameters": [ + { + "description": "request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.ContainerOperate" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + }, + "x-panel-log": { + "BeforeFunctions": [], + "bodyKeys": [ + "name", + "image" + ], + "formatEN": "update container [name][image]", + "formatZH": "更新容器 [name][image]", + "paramKeys": [] + } + } + }, + "/containers/upgrade": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "更新容器镜像", + "consumes": [ + "application/json" + ], + "tags": [ + "Container" + ], + "summary": "Upgrade container", + "parameters": [ + { + "description": "request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.ContainerUpgrade" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + }, + "x-panel-log": { + "BeforeFunctions": [], + "bodyKeys": [ + "name", + "image" + ], + "formatEN": "upgrade container image [name][image]", + "formatZH": "更新容器镜像 [name][image]", + "paramKeys": [] + } + } + }, + "/containers/volume": { + "get": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "获取容器存储卷列表", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Container Volume" + ], + "summary": "List volumes", + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/dto.Options" + } + } + } + } + }, + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "创建容器存储卷", + "consumes": [ + "application/json" + ], + "tags": [ + "Container Volume" + ], + "summary": "Create volume", + "parameters": [ + { + "description": "request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.VolumeCreate" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + }, + "x-panel-log": { + "BeforeFunctions": [], + "bodyKeys": [ + "name" + ], + "formatEN": "create container volume [name]", + "formatZH": "创建容器存储卷 [name]", + "paramKeys": [] + } + } + }, + "/containers/volume/del": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "删除容器存储卷", + "consumes": [ + "application/json" + ], + "tags": [ + "Container Volume" + ], + "summary": "Delete volume", + "parameters": [ + { + "description": "request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.BatchDelete" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + }, + "x-panel-log": { + "BeforeFunctions": [], + "bodyKeys": [ + "names" + ], + "formatEN": "delete container volume [names]", + "formatZH": "删除容器存储卷 [names]", + "paramKeys": [] + } + } + }, + "/containers/volume/search": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "获取容器存储卷分页", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Container Volume" + ], + "summary": "Page volumes", + "parameters": [ + { + "description": "request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.SearchWithPage" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/dto.PageResult" + } + } + } + } + }, + "/cronjobs": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "创建计划任务", + "consumes": [ + "application/json" + ], + "tags": [ + "Cronjob" + ], + "summary": "Create cronjob", + "parameters": [ + { + "description": "request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.CronjobCreate" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + }, + "x-panel-log": { + "BeforeFunctions": [], + "bodyKeys": [ + "type", + "name" + ], + "formatEN": "create cronjob [type][name]", + "formatZH": "创建计划任务 [type][name]", + "paramKeys": [] + } + } + }, + "/cronjobs/del": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "删除计划任务", + "consumes": [ + "application/json" + ], + "tags": [ + "Cronjob" + ], + "summary": "Delete cronjob", + "parameters": [ + { + "description": "request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.CronjobBatchDelete" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + }, + "x-panel-log": { + "BeforeFunctions": [ + { + "db": "cronjobs", + "input_column": "id", + "input_value": "ids", + "isList": true, + "output_column": "name", + "output_value": "names" + } + ], + "bodyKeys": [ + "ids" + ], + "formatEN": "delete cronjob [names]", + "formatZH": "删除计划任务 [names]", + "paramKeys": [] + } + } + }, + "/cronjobs/download": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "下载计划任务记录", + "consumes": [ + "application/json" + ], + "tags": [ + "Cronjob" + ], + "summary": "Download cronjob records", + "parameters": [ + { + "description": "request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.CronjobDownload" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + }, + "x-panel-log": { + "BeforeFunctions": [ + { + "db": "job_records", + "input_column": "id", + "input_value": "recordID", + "isList": false, + "output_column": "file", + "output_value": "file" + } + ], + "bodyKeys": [ + "recordID" + ], + "formatEN": "download the cronjob record [file]", + "formatZH": "下载计划任务记录 [file]", + "paramKeys": [] + } + } + }, + "/cronjobs/handle": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "手动执行计划任务", + "consumes": [ + "application/json" + ], + "tags": [ + "Cronjob" + ], + "summary": "Handle cronjob once", + "parameters": [ + { + "description": "request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.OperateByID" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + }, + "x-panel-log": { + "BeforeFunctions": [ + { + "db": "cronjobs", + "input_column": "id", + "input_value": "id", + "isList": false, + "output_column": "name", + "output_value": "name" + } + ], + "bodyKeys": [ + "id" + ], + "formatEN": "manually execute the cronjob [name]", + "formatZH": "手动执行计划任务 [name]", + "paramKeys": [] + } + } + }, + "/cronjobs/records/clean": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "清空计划任务记录", + "consumes": [ + "application/json" + ], + "tags": [ + "Cronjob" + ], + "summary": "Clean job records", + "parameters": [ + { + "description": "request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.CronjobClean" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + }, + "x-panel-log": { + "BeforeFunctions": [ + { + "db": "cronjobs", + "input_column": "id", + "input_value": "id", + "isList": false, + "output_column": "name", + "output_value": "name" + } + ], + "bodyKeys": [ + "id" + ], + "formatEN": "clean cronjob [name] records", + "formatZH": "清空计划任务记录 [name]", + "paramKeys": [] + } + } + }, + "/cronjobs/records/log": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "获取计划任务记录日志", + "consumes": [ + "application/json" + ], + "tags": [ + "Cronjob" + ], + "summary": "Load Cronjob record log", + "parameters": [ + { + "description": "request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.OperateByID" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + } + } + }, + "/cronjobs/search": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "获取计划任务分页", + "consumes": [ + "application/json" + ], + "tags": [ + "Cronjob" + ], + "summary": "Page cronjobs", + "parameters": [ + { + "description": "request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.PageCronjob" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/dto.PageResult" + } + } + } + } + }, + "/cronjobs/search/records": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "获取计划任务记录", + "consumes": [ + "application/json" + ], + "tags": [ + "Cronjob" + ], + "summary": "Page job records", + "parameters": [ + { + "description": "request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.SearchRecord" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/dto.PageResult" + } + } + } + } + }, + "/cronjobs/status": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "更新计划任务状态", + "consumes": [ + "application/json" + ], + "tags": [ + "Cronjob" + ], + "summary": "Update cronjob status", + "parameters": [ + { + "description": "request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.CronjobUpdateStatus" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + }, + "x-panel-log": { + "BeforeFunctions": [ + { + "db": "cronjobs", + "input_column": "id", + "input_value": "id", + "isList": false, + "output_column": "name", + "output_value": "name" + } + ], + "bodyKeys": [ + "id", + "status" + ], + "formatEN": "change the status of cronjob [name] to [status].", + "formatZH": "修改计划任务 [name] 状态为 [status]", + "paramKeys": [] + } + } + }, + "/cronjobs/update": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "更新计划任务", + "consumes": [ + "application/json" + ], + "tags": [ + "Cronjob" + ], + "summary": "Update cronjob", + "parameters": [ + { + "description": "request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.CronjobUpdate" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + }, + "x-panel-log": { + "BeforeFunctions": [ + { + "db": "cronjobs", + "input_column": "id", + "input_value": "id", + "isList": false, + "output_column": "name", + "output_value": "name" + } + ], + "bodyKeys": [ + "id" + ], + "formatEN": "update cronjob [name]", + "formatZH": "更新计划任务 [name]", + "paramKeys": [] + } + } + }, + "/dashboard/base/:ioOption/:netOption": { + "get": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "获取首页基础数据", + "consumes": [ + "application/json" + ], + "tags": [ + "Dashboard" + ], + "summary": "Load dashboard base info", + "parameters": [ + { + "type": "string", + "description": "request", + "name": "ioOption", + "in": "path", + "required": true + }, + { + "type": "string", + "description": "request", + "name": "netOption", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/dto.DashboardBase" + } + } + } + } + }, + "/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": [ + { + "ApiKeyAuth": [] + } + ], + "description": "获取首页实时数据", + "consumes": [ + "application/json" + ], + "tags": [ + "Dashboard" + ], + "summary": "Load dashboard current info", + "parameters": [ + { + "type": "string", + "description": "request", + "name": "ioOption", + "in": "path", + "required": true + }, + { + "type": "string", + "description": "request", + "name": "netOption", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/dto.DashboardCurrent" + } + } + } + } + }, + "/dashboard/system/restart/:operation": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "重启服务器/面板", + "consumes": [ + "application/json" + ], + "tags": [ + "Dashboard" + ], + "summary": "System restart", + "parameters": [ + { + "type": "string", + "description": "request", + "name": "operation", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK" + } + } + } + }, + "/databases": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "创建 mysql 数据库", + "consumes": [ + "application/json" + ], + "tags": [ + "Database Mysql" + ], + "summary": "Create mysql database", + "parameters": [ + { + "description": "request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.MysqlDBCreate" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + }, + "x-panel-log": { + "BeforeFunctions": [], + "bodyKeys": [ + "name" + ], + "formatEN": "create mysql database [name]", + "formatZH": "创建 mysql 数据库 [name]", + "paramKeys": [] + } + } + }, + "/databases/bind": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "绑定 mysql 数据库用户", + "consumes": [ + "application/json" + ], + "tags": [ + "Database Mysql" + ], + "summary": "Bind user of mysql database", + "parameters": [ + { + "description": "request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.BindUser" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + }, + "x-panel-log": { + "BeforeFunctions": [], + "bodyKeys": [ + "database", + "username" + ], + "formatEN": "bind mysql database [database] [username]", + "formatZH": "绑定 mysql 数据库名 [database] [username]", + "paramKeys": [] + } + } + }, + "/databases/change/access": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "修改 mysql 访问权限", + "consumes": [ + "application/json" + ], + "tags": [ + "Database Mysql" + ], + "summary": "Change mysql access", + "parameters": [ + { + "description": "request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.ChangeDBInfo" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + }, + "x-panel-log": { + "BeforeFunctions": [ + { + "db": "database_mysqls", + "input_column": "id", + "input_value": "id", + "isList": false, + "output_column": "name", + "output_value": "name" + } + ], + "bodyKeys": [ + "id" + ], + "formatEN": "Update database [name] access", + "formatZH": "更新数据库 [name] 访问权限", + "paramKeys": [] + } + } + }, + "/databases/change/password": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "修改 mysql 密码", + "consumes": [ + "application/json" + ], + "tags": [ + "Database Mysql" + ], + "summary": "Change mysql password", + "parameters": [ + { + "description": "request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.ChangeDBInfo" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + }, + "x-panel-log": { + "BeforeFunctions": [ + { + "db": "database_mysqls", + "input_column": "id", + "input_value": "id", + "isList": false, + "output_column": "name", + "output_value": "name" + } + ], + "bodyKeys": [ + "id" + ], + "formatEN": "Update database [name] password", + "formatZH": "更新数据库 [name] 密码", + "paramKeys": [] + } + } + }, + "/databases/common/info": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "获取数据库基础信息", + "consumes": [ + "application/json" + ], + "tags": [ + "Database Common" + ], + "summary": "Load base info", + "parameters": [ + { + "description": "request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.OperationWithNameAndType" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/dto.DBBaseInfo" + } + } + } + } + }, + "/databases/common/load/file": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "获取数据库配置文件", + "consumes": [ + "application/json" + ], + "tags": [ + "Database Common" + ], + "summary": "Load Database conf", + "parameters": [ + { + "description": "request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.OperationWithNameAndType" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + } + } + }, + "/databases/common/update/conf": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "上传替换配置文件", + "consumes": [ + "application/json" + ], + "tags": [ + "Database Common" + ], + "summary": "Update conf by upload file", + "parameters": [ + { + "description": "request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.DBConfUpdateByFile" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + }, + "x-panel-log": { + "BeforeFunctions": [], + "bodyKeys": [ + "type", + "database" + ], + "formatEN": "update the [type] [database] database configuration information", + "formatZH": "更新 [type] 数据库 [database] 配置信息", + "paramKeys": [] + } + } + }, + "/databases/db": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "创建远程数据库", + "consumes": [ + "application/json" + ], + "tags": [ + "Database" + ], + "summary": "Create database", + "parameters": [ + { + "description": "request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.DatabaseCreate" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + }, + "x-panel-log": { + "BeforeFunctions": [], + "bodyKeys": [ + "name", + "type" + ], + "formatEN": "create database [name][type]", + "formatZH": "创建远程数据库 [name][type]", + "paramKeys": [] + } + } + }, + "/databases/db/:name": { + "get": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "获取远程数据库", + "tags": [ + "Database" + ], + "summary": "Get databases", + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/dto.DatabaseInfo" + } + } + } + } + }, + "/databases/db/check": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "检测远程数据库连接性", + "consumes": [ + "application/json" + ], + "tags": [ + "Database" + ], + "summary": "Check database", + "parameters": [ + { + "description": "request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.DatabaseCreate" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + }, + "x-panel-log": { + "BeforeFunctions": [], + "bodyKeys": [ + "name", + "type" + ], + "formatEN": "check if database [name][type] is connectable", + "formatZH": "检测远程数据库 [name][type] 连接性", + "paramKeys": [] + } + } + }, + "/databases/db/del": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "删除远程数据库", + "consumes": [ + "application/json" + ], + "tags": [ + "Database" + ], + "summary": "Delete database", + "parameters": [ + { + "description": "request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.DatabaseDelete" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + }, + "x-panel-log": { + "BeforeFunctions": [ + { + "db": "databases", + "input_column": "id", + "input_value": "ids", + "isList": true, + "output_column": "name", + "output_value": "names" + } + ], + "bodyKeys": [ + "ids" + ], + "formatEN": "delete database [names]", + "formatZH": "删除远程数据库 [names]", + "paramKeys": [] + } + } + }, + "/databases/db/item/:type": { + "get": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "获取数据库列表", + "tags": [ + "Database" + ], + "summary": "List databases", + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/dto.DatabaseItem" + } + } + } + } + } + }, + "/databases/db/list/:type": { + "get": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "获取远程数据库列表", + "tags": [ + "Database" + ], + "summary": "List databases", + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/dto.DatabaseOption" + } + } + } + } + } + }, + "/databases/db/search": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "获取远程数据库列表分页", + "consumes": [ + "application/json" + ], + "tags": [ + "Database" + ], + "summary": "Page databases", + "parameters": [ + { + "description": "request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.DatabaseSearch" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/dto.PageResult" + } + } + } + } + }, + "/databases/db/update": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "更新远程数据库", + "consumes": [ + "application/json" + ], + "tags": [ + "Database" + ], + "summary": "Update database", + "parameters": [ + { + "description": "request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.DatabaseUpdate" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + }, + "x-panel-log": { + "BeforeFunctions": [], + "bodyKeys": [ + "name" + ], + "formatEN": "update database [name]", + "formatZH": "更新远程数据库 [name]", + "paramKeys": [] + } + } + }, + "/databases/del": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "删除 mysql 数据库", + "consumes": [ + "application/json" + ], + "tags": [ + "Database Mysql" + ], + "summary": "Delete mysql database", + "parameters": [ + { + "description": "request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.MysqlDBDelete" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + }, + "x-panel-log": { + "BeforeFunctions": [ + { + "db": "database_mysqls", + "input_column": "id", + "input_value": "id", + "isList": false, + "output_column": "name", + "output_value": "name" + } + ], + "bodyKeys": [ + "id" + ], + "formatEN": "delete mysql database [name]", + "formatZH": "删除 mysql 数据库 [name]", + "paramKeys": [] + } + } + }, + "/databases/del/check": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "Mysql 数据库删除前检查", + "consumes": [ + "application/json" + ], + "tags": [ + "Database Mysql" + ], + "summary": "Check before delete mysql database", + "parameters": [ + { + "description": "request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.MysqlDBDeleteCheck" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "array", + "items": { + "type": "string" + } + } + } + } + } + }, + "/databases/description/update": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "更新 mysql 数据库库描述信息", + "consumes": [ + "application/json" + ], + "tags": [ + "Database Mysql" + ], + "summary": "Update mysql database description", + "parameters": [ + { + "description": "request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.UpdateDescription" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + }, + "x-panel-log": { + "BeforeFunctions": [ + { + "db": "database_mysqls", + "input_column": "id", + "input_value": "id", + "isList": false, + "output_column": "name", + "output_value": "name" + } + ], + "bodyKeys": [ + "id", + "description" + ], + "formatEN": "The description of the mysql database [name] is modified =\u003e [description]", + "formatZH": "mysql 数据库 [name] 描述信息修改 [description]", + "paramKeys": [] + } + } + }, + "/databases/load": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "从服务器获取", + "consumes": [ + "application/json" + ], + "tags": [ + "Database Mysql" + ], + "summary": "Load mysql database from remote", + "parameters": [ + { + "description": "request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.MysqlLoadDB" + } + } + ], + "responses": {} + } + }, + "/databases/options": { + "get": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "获取 mysql 数据库列表", + "consumes": [ + "application/json" + ], + "tags": [ + "Database Mysql" + ], + "summary": "List mysql database names", + "parameters": [ + { + "description": "request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.PageInfo" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/dto.MysqlOption" + } + } + } + } + } + }, + "/databases/pg": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "创建 postgresql 数据库", + "consumes": [ + "application/json" + ], + "tags": [ + "Database Postgresql" + ], + "summary": "Create postgresql database", + "parameters": [ + { + "description": "request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.PostgresqlDBCreate" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + }, + "x-panel-log": { + "BeforeFunctions": [], + "bodyKeys": [ + "name" + ], + "formatEN": "create postgresql database [name]", + "formatZH": "创建 postgresql 数据库 [name]", + "paramKeys": [] + } + } + }, + "/databases/pg/:database/load": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "从服务器获取", + "consumes": [ + "application/json" + ], + "tags": [ + "Database Postgresql" + ], + "summary": "Load postgresql database from remote", + "parameters": [ + { + "description": "request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.PostgresqlLoadDB" + } + } + ], + "responses": {} + } + }, + "/databases/pg/bind": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "绑定 postgresql 数据库用户", + "consumes": [ + "application/json" + ], + "tags": [ + "Database Postgresql" + ], + "summary": "Bind postgresql user", + "parameters": [ + { + "description": "request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.PostgresqlBindUser" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + }, + "x-panel-log": { + "BeforeFunctions": [], + "bodyKeys": [ + "name", + "username" + ], + "formatEN": "bind postgresql database [name] user [username]", + "formatZH": "绑定 postgresql 数据库 [name] 用户 [username]", + "paramKeys": [] + } + } + }, + "/databases/pg/del": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "删除 postgresql 数据库", + "consumes": [ + "application/json" + ], + "tags": [ + "Database Postgresql" + ], + "summary": "Delete postgresql database", + "parameters": [ + { + "description": "request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.PostgresqlDBDelete" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + }, + "x-panel-log": { + "BeforeFunctions": [ + { + "db": "database_postgresqls", + "input_column": "id", + "input_value": "id", + "isList": false, + "output_column": "name", + "output_value": "name" + } + ], + "bodyKeys": [ + "id" + ], + "formatEN": "delete postgresql database [name]", + "formatZH": "删除 postgresql 数据库 [name]", + "paramKeys": [] + } + } + }, + "/databases/pg/del/check": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "Postgresql 数据库删除前检查", + "consumes": [ + "application/json" + ], + "tags": [ + "Database Postgresql" + ], + "summary": "Check before delete postgresql database", + "parameters": [ + { + "description": "request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.PostgresqlDBDeleteCheck" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "array", + "items": { + "type": "string" + } + } + } + } + } + }, + "/databases/pg/description": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "更新 postgresql 数据库库描述信息", + "consumes": [ + "application/json" + ], + "tags": [ + "Database Postgresql" + ], + "summary": "Update postgresql database description", + "parameters": [ + { + "description": "request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.UpdateDescription" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + }, + "x-panel-log": { + "BeforeFunctions": [ + { + "db": "database_postgresqls", + "input_column": "id", + "input_value": "id", + "isList": false, + "output_column": "name", + "output_value": "name" + } + ], + "bodyKeys": [ + "id", + "description" + ], + "formatEN": "The description of the postgresql database [name] is modified =\u003e [description]", + "formatZH": "postgresql 数据库 [name] 描述信息修改 [description]", + "paramKeys": [] + } + } + }, + "/databases/pg/password": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "修改 postgresql 密码", + "consumes": [ + "application/json" + ], + "tags": [ + "Database Postgresql" + ], + "summary": "Change postgresql password", + "parameters": [ + { + "description": "request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.ChangeDBInfo" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + }, + "x-panel-log": { + "BeforeFunctions": [ + { + "db": "database_postgresqls", + "input_column": "id", + "input_value": "id", + "isList": false, + "output_column": "name", + "output_value": "name" + } + ], + "bodyKeys": [ + "id" + ], + "formatEN": "Update database [name] password", + "formatZH": "更新数据库 [name] 密码", + "paramKeys": [] + } + } + }, + "/databases/pg/privileges": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "修改 postgresql 用户权限", + "consumes": [ + "application/json" + ], + "tags": [ + "Database Postgresql" + ], + "summary": "Change postgresql privileges", + "parameters": [ + { + "description": "request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.ChangeDBInfo" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + }, + "x-panel-log": { + "BeforeFunctions": [], + "bodyKeys": [ + "database", + "username" + ], + "formatEN": "Update [user] privileges of database [database]", + "formatZH": "更新数据库 [database] 用户 [username] 权限", + "paramKeys": [] + } + } + }, + "/databases/pg/search": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "获取 postgresql 数据库列表分页", + "consumes": [ + "application/json" + ], + "tags": [ + "Database Postgresql" + ], + "summary": "Page postgresql databases", + "parameters": [ + { + "description": "request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.PostgresqlDBSearch" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/dto.PageResult" + } + } + } + } + }, + "/databases/redis/conf": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "获取 redis 配置信息", + "consumes": [ + "application/json" + ], + "tags": [ + "Database Redis" + ], + "summary": "Load redis conf", + "parameters": [ + { + "description": "request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.OperationWithName" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/dto.RedisConf" + } + } + } + } + }, + "/databases/redis/conf/update": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "更新 redis 配置信息", + "consumes": [ + "application/json" + ], + "tags": [ + "Database Redis" + ], + "summary": "Update redis conf", + "parameters": [ + { + "description": "request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.RedisConfUpdate" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + }, + "x-panel-log": { + "BeforeFunctions": [], + "bodyKeys": [], + "formatEN": "update the redis database configuration information", + "formatZH": "更新 redis 数据库配置信息", + "paramKeys": [] + } + } + }, + "/databases/redis/install/cli": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "安装 redis cli", + "tags": [ + "Database Redis" + ], + "summary": "Install redis-cli", + "responses": { + "200": { + "description": "OK" + } + } + } + }, + "/databases/redis/password": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "更新 redis 密码", + "consumes": [ + "application/json" + ], + "tags": [ + "Database Redis" + ], + "summary": "Change redis password", + "parameters": [ + { + "description": "request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.ChangeRedisPass" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + }, + "x-panel-log": { + "BeforeFunctions": [], + "bodyKeys": [], + "formatEN": "change the password of the redis database", + "formatZH": "修改 redis 数据库密码", + "paramKeys": [] + } + } + }, + "/databases/redis/persistence/conf": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "获取 redis 持久化配置", + "consumes": [ + "application/json" + ], + "tags": [ + "Database Redis" + ], + "summary": "Load redis persistence conf", + "parameters": [ + { + "description": "request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.OperationWithName" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/dto.RedisPersistence" + } + } + } + } + }, + "/databases/redis/persistence/update": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "更新 redis 持久化配置", + "consumes": [ + "application/json" + ], + "tags": [ + "Database Redis" + ], + "summary": "Update redis persistence conf", + "parameters": [ + { + "description": "request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.RedisConfPersistenceUpdate" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + }, + "x-panel-log": { + "BeforeFunctions": [], + "bodyKeys": [], + "formatEN": "redis database persistence configuration update", + "formatZH": "redis 数据库持久化配置更新", + "paramKeys": [] + } + } + }, + "/databases/redis/status": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "获取 redis 状态信息", + "consumes": [ + "application/json" + ], + "tags": [ + "Database Redis" + ], + "summary": "Load redis status info", + "parameters": [ + { + "description": "request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.OperationWithName" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/dto.RedisStatus" + } + } + } + } + }, + "/databases/remote": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "获取 mysql 远程访问权限", + "consumes": [ + "application/json" + ], + "tags": [ + "Database Mysql" + ], + "summary": "Load mysql remote access", + "parameters": [ + { + "description": "request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.OperationWithNameAndType" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "boolean" + } + } + } + } + }, + "/databases/search": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "获取 mysql 数据库列表分页", + "consumes": [ + "application/json" + ], + "tags": [ + "Database Mysql" + ], + "summary": "Page mysql databases", + "parameters": [ + { + "description": "request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.MysqlDBSearch" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/dto.PageResult" + } + } + } + } + }, + "/databases/status": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "获取 mysql 状态信息", + "consumes": [ + "application/json" + ], + "tags": [ + "Database Mysql" + ], + "summary": "Load mysql status info", + "parameters": [ + { + "description": "request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.OperationWithNameAndType" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/dto.MysqlStatus" + } + } + } + } + }, + "/databases/variables": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "获取 mysql 性能参数信息", + "consumes": [ + "application/json" + ], + "tags": [ + "Database Mysql" + ], + "summary": "Load mysql variables info", + "parameters": [ + { + "description": "request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.OperationWithNameAndType" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/dto.MysqlVariables" + } + } + } + } + }, + "/databases/variables/update": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "mysql 性能调优", + "consumes": [ + "application/json" + ], + "tags": [ + "Database Mysql" + ], + "summary": "Update mysql variables", + "parameters": [ + { + "description": "request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.MysqlVariablesUpdate" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + }, + "x-panel-log": { + "BeforeFunctions": [], + "bodyKeys": [], + "formatEN": "adjust mysql database performance parameters", + "formatZH": "调整 mysql 数据库性能参数", + "paramKeys": [] + } + } + }, + "/db/remote/del/check": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "Mysql 远程数据库删除前检查", + "consumes": [ + "application/json" + ], + "tags": [ + "Database" + ], + "summary": "Check before delete remote database", + "parameters": [ + { + "description": "request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.OperateByID" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "array", + "items": { + "type": "string" + } + } + } + } + } + }, + "/files": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "创建文件/文件夹", + "consumes": [ + "application/json" + ], + "tags": [ + "File" + ], + "summary": "Create file", + "parameters": [ + { + "description": "request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/request.FileCreate" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + }, + "x-panel-log": { + "BeforeFunctions": [], + "bodyKeys": [ + "path" + ], + "formatEN": "Create dir or file [path]", + "formatZH": "创建文件/文件夹 [path]", + "paramKeys": [] + } + } + }, + "/files/batch/del": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "批量删除文件/文件夹", + "consumes": [ + "application/json" + ], + "tags": [ + "File" + ], + "summary": "Batch delete file", + "parameters": [ + { + "description": "request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/request.FileBatchDelete" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + }, + "x-panel-log": { + "BeforeFunctions": [], + "bodyKeys": [ + "paths" + ], + "formatEN": "Batch delete dir or file [paths]", + "formatZH": "批量删除文件/文件夹 [paths]", + "paramKeys": [] + } + } + }, + "/files/batch/role": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "批量修改文件权限和用户/组", + "consumes": [ + "application/json" + ], + "tags": [ + "File" + ], + "summary": "Batch change file mode and owner", + "parameters": [ + { + "description": "request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/request.FileRoleReq" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + }, + "x-panel-log": { + "BeforeFunctions": [], + "bodyKeys": [ + "paths", + "mode", + "user", + "group" + ], + "formatEN": "Batch change file mode and owner [paths] =\u003e [mode]/[user]/[group]", + "formatZH": "批量修改文件权限和用户/组 [paths] =\u003e [mode]/[user]/[group]", + "paramKeys": [] + } + } + }, + "/files/check": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "检测文件是否存在", + "consumes": [ + "application/json" + ], + "tags": [ + "File" + ], + "summary": "Check file exist", + "parameters": [ + { + "description": "request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/request.FilePathCheck" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + } + } + }, + "/files/chunkdownload": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "分片下载下载文件", + "consumes": [ + "application/json" + ], + "tags": [ + "File" + ], + "summary": "Chunk Download file", + "parameters": [ + { + "description": "request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/request.FileDownload" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + }, + "x-panel-log": { + "BeforeFunctions": [], + "bodyKeys": [ + "name" + ], + "formatEN": "Download file [name]", + "formatZH": "下载文件 [name]", + "paramKeys": [] + } + } + }, + "/files/chunkupload": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "分片上传文件", + "tags": [ + "File" + ], + "summary": "ChunkUpload file", + "parameters": [ + { + "type": "file", + "description": "request", + "name": "file", + "in": "formData", + "required": true + } + ], + "responses": { + "200": { + "description": "OK" + } + } + } + }, + "/files/compress": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "压缩文件", + "consumes": [ + "application/json" + ], + "tags": [ + "File" + ], + "summary": "Compress file", + "parameters": [ + { + "description": "request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/request.FileCompress" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + }, + "x-panel-log": { + "BeforeFunctions": [], + "bodyKeys": [ + "name" + ], + "formatEN": "Compress file [name]", + "formatZH": "压缩文件 [name]", + "paramKeys": [] + } + } + }, + "/files/content": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "获取文件内容", + "consumes": [ + "application/json" + ], + "tags": [ + "File" + ], + "summary": "Load file content", + "parameters": [ + { + "description": "request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/request.FileContentReq" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/response.FileInfo" + } + } + }, + "x-panel-log": { + "BeforeFunctions": [], + "bodyKeys": [ + "path" + ], + "formatEN": "Load file content [path]", + "formatZH": "获取文件内容 [path]", + "paramKeys": [] + } + } + }, + "/files/decompress": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "解压文件", + "consumes": [ + "application/json" + ], + "tags": [ + "File" + ], + "summary": "Decompress file", + "parameters": [ + { + "description": "request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/request.FileDeCompress" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + }, + "x-panel-log": { + "BeforeFunctions": [], + "bodyKeys": [ + "path" + ], + "formatEN": "Decompress file [path]", + "formatZH": "解压 [path]", + "paramKeys": [] + } + } + }, + "/files/del": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "删除文件/文件夹", + "consumes": [ + "application/json" + ], + "tags": [ + "File" + ], + "summary": "Delete file", + "parameters": [ + { + "description": "request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/request.FileDelete" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + }, + "x-panel-log": { + "BeforeFunctions": [], + "bodyKeys": [ + "path" + ], + "formatEN": "Delete dir or file [path]", + "formatZH": "删除文件/文件夹 [path]", + "paramKeys": [] + } + } + }, + "/files/download": { + "get": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "下载文件", + "consumes": [ + "application/json" + ], + "tags": [ + "File" + ], + "summary": "Download file", + "responses": { + "200": { + "description": "OK" + } + } + } + }, + "/files/favorite": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "创建收藏", + "consumes": [ + "application/json" + ], + "tags": [ + "File" + ], + "summary": "Create favorite", + "parameters": [ + { + "description": "request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/request.FavoriteCreate" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + }, + "x-panel-log": { + "BeforeFunctions": [], + "bodyKeys": [ + "path" + ], + "formatEN": "收藏文件/文件夹 [path]", + "formatZH": "收藏文件/文件夹 [path]", + "paramKeys": [] + } + } + }, + "/files/favorite/del": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "删除收藏", + "consumes": [ + "application/json" + ], + "tags": [ + "File" + ], + "summary": "Delete favorite", + "parameters": [ + { + "description": "request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/request.FavoriteDelete" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + }, + "x-panel-log": { + "BeforeFunctions": [ + { + "db": "favorites", + "input_column": "id", + "input_value": "id", + "isList": false, + "output_column": "path", + "output_value": "path" + } + ], + "bodyKeys": [ + "id" + ], + "formatEN": "delete avorite [path]", + "formatZH": "删除收藏 [path]", + "paramKeys": [] + } + } + }, + "/files/favorite/search": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "获取收藏列表", + "consumes": [ + "application/json" + ], + "tags": [ + "File" + ], + "summary": "List favorites", + "parameters": [ + { + "description": "request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.PageInfo" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + } + } + }, + "/files/mode": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "修改文件权限", + "consumes": [ + "application/json" + ], + "tags": [ + "File" + ], + "summary": "Change file mode", + "parameters": [ + { + "description": "request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/request.FileCreate" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + }, + "x-panel-log": { + "BeforeFunctions": [], + "bodyKeys": [ + "path", + "mode" + ], + "formatEN": "Change mode [paths] =\u003e [mode]", + "formatZH": "修改权限 [paths] =\u003e [mode]", + "paramKeys": [] + } + } + }, + "/files/move": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "移动文件", + "consumes": [ + "application/json" + ], + "tags": [ + "File" + ], + "summary": "Move file", + "parameters": [ + { + "description": "request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/request.FileMove" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + }, + "x-panel-log": { + "BeforeFunctions": [], + "bodyKeys": [ + "oldPaths", + "newPath" + ], + "formatEN": "Move [oldPaths] =\u003e [newPath]", + "formatZH": "移动文件 [oldPaths] =\u003e [newPath]", + "paramKeys": [] + } + } + }, + "/files/owner": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "修改文件用户/组", + "consumes": [ + "application/json" + ], + "tags": [ + "File" + ], + "summary": "Change file owner", + "parameters": [ + { + "description": "request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/request.FileRoleUpdate" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + }, + "x-panel-log": { + "BeforeFunctions": [], + "bodyKeys": [ + "path", + "user", + "group" + ], + "formatEN": "Change owner [paths] =\u003e [user]/[group]", + "formatZH": "修改用户/组 [paths] =\u003e [user]/[group]", + "paramKeys": [] + } + } + }, + "/files/read": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "按行读取日志文件", + "tags": [ + "File" + ], + "summary": "Read file by Line", + "parameters": [ + { + "description": "request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/request.FileReadByLineReq" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + } + } + }, + "/files/recycle/clear": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "清空回收站文件", + "consumes": [ + "application/json" + ], + "tags": [ + "File" + ], + "summary": "Clear RecycleBin files", + "responses": { + "200": { + "description": "OK" + } + }, + "x-panel-log": { + "BeforeFunctions": [], + "bodyKeys": [], + "formatEN": "清空回收站", + "formatZH": "清空回收站", + "paramKeys": [] + } + } + }, + "/files/recycle/reduce": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "还原回收站文件", + "consumes": [ + "application/json" + ], + "tags": [ + "File" + ], + "summary": "Reduce RecycleBin files", + "parameters": [ + { + "description": "request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/request.RecycleBinReduce" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + }, + "x-panel-log": { + "BeforeFunctions": [], + "bodyKeys": [ + "name" + ], + "formatEN": "Reduce RecycleBin file [name]", + "formatZH": "还原回收站文件 [name]", + "paramKeys": [] + } + } + }, + "/files/recycle/search": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "获取回收站文件列表", + "consumes": [ + "application/json" + ], + "tags": [ + "File" + ], + "summary": "List RecycleBin files", + "parameters": [ + { + "description": "request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.PageInfo" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + } + } + }, + "/files/recycle/status": { + "get": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "获取回收站状态", + "consumes": [ + "application/json" + ], + "tags": [ + "File" + ], + "summary": "Get RecycleBin status", + "responses": { + "200": { + "description": "OK" + } + } + } + }, + "/files/rename": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "修改文件名称", + "consumes": [ + "application/json" + ], + "tags": [ + "File" + ], + "summary": "Change file name", + "parameters": [ + { + "description": "request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/request.FileRename" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + }, + "x-panel-log": { + "BeforeFunctions": [], + "bodyKeys": [ + "oldName", + "newName" + ], + "formatEN": "Rename [oldName] =\u003e [newName]", + "formatZH": "重命名 [oldName] =\u003e [newName]", + "paramKeys": [] + } + } + }, + "/files/save": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "更新文件内容", + "consumes": [ + "application/json" + ], + "tags": [ + "File" + ], + "summary": "Update file content", + "parameters": [ + { + "description": "request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/request.FileEdit" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + }, + "x-panel-log": { + "BeforeFunctions": [], + "bodyKeys": [ + "path" + ], + "formatEN": "Update file content [path]", + "formatZH": "更新文件内容 [path]", + "paramKeys": [] + } + } + }, + "/files/search": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "获取文件列表", + "consumes": [ + "application/json" + ], + "tags": [ + "File" + ], + "summary": "List files", + "parameters": [ + { + "description": "request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/request.FileOption" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/response.FileInfo" + } + } + } + } + }, + "/files/size": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "获取文件夹大小", + "consumes": [ + "application/json" + ], + "tags": [ + "File" + ], + "summary": "Load file size", + "parameters": [ + { + "description": "request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/request.DirSizeReq" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + }, + "x-panel-log": { + "BeforeFunctions": [], + "bodyKeys": [ + "path" + ], + "formatEN": "Load file size [path]", + "formatZH": "获取文件夹大小 [path]", + "paramKeys": [] + } + } + }, + "/files/tree": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "加载文件树", + "consumes": [ + "application/json" + ], + "tags": [ + "File" + ], + "summary": "Load files tree", + "parameters": [ + { + "description": "request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/request.FileOption" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/response.FileTree" + } + } + } + } + } + }, + "/files/upload": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "上传文件", + "tags": [ + "File" + ], + "summary": "Upload file", + "parameters": [ + { + "type": "file", + "description": "request", + "name": "file", + "in": "formData", + "required": true + } + ], + "responses": { + "200": { + "description": "OK" + } + }, + "x-panel-log": { + "BeforeFunctions": [], + "bodyKeys": [ + "path" + ], + "formatEN": "Upload file [path]", + "formatZH": "上传文件 [path]", + "paramKeys": [] + } + } + }, + "/files/upload/search": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "分页获取上传文件", + "consumes": [ + "application/json" + ], + "tags": [ + "File" + ], + "summary": "Page file", + "parameters": [ + { + "description": "request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/request.SearchUploadWithPage" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/response.FileInfo" + } + } + } + } + } + }, + "/files/wget": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "下载远端文件", + "consumes": [ + "application/json" + ], + "tags": [ + "File" + ], + "summary": "Wget file", + "parameters": [ + { + "description": "request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/request.FileWget" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + }, + "x-panel-log": { + "BeforeFunctions": [], + "bodyKeys": [ + "url", + "path", + "name" + ], + "formatEN": "Download url =\u003e [path]/[name]", + "formatZH": "下载 url =\u003e [path]/[name]", + "paramKeys": [] + } + } + }, + "/groups": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "创建系统组", + "consumes": [ + "application/json" + ], + "tags": [ + "System Group" + ], + "summary": "Create group", + "parameters": [ + { + "description": "request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.GroupCreate" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + }, + "x-panel-log": { + "BeforeFunctions": [], + "bodyKeys": [ + "name", + "type" + ], + "formatEN": "create group [name][type]", + "formatZH": "创建组 [name][type]", + "paramKeys": [] + } + } + }, + "/groups/del": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "删除系统组", + "consumes": [ + "application/json" + ], + "tags": [ + "System Group" + ], + "summary": "Delete group", + "parameters": [ + { + "description": "request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.OperateByID" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + }, + "x-panel-log": { + "BeforeFunctions": [ + { + "db": "groups", + "input_column": "id", + "input_value": "id", + "isList": false, + "output_column": "name", + "output_value": "name" + }, + { + "db": "groups", + "input_column": "id", + "input_value": "id", + "isList": false, + "output_column": "type", + "output_value": "type" + } + ], + "bodyKeys": [ + "id" + ], + "formatEN": "delete group [type][name]", + "formatZH": "删除组 [type][name]", + "paramKeys": [] + } + } + }, + "/groups/search": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "查询系统组", + "consumes": [ + "application/json" + ], + "tags": [ + "System Group" + ], + "summary": "List groups", + "parameters": [ + { + "description": "request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.GroupSearch" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/dto.GroupInfo" + } + } + } + } + } + }, + "/groups/update": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "更新系统组", + "consumes": [ + "application/json" + ], + "tags": [ + "System Group" + ], + "summary": "Update group", + "parameters": [ + { + "description": "request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.GroupUpdate" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + }, + "x-panel-log": { + "BeforeFunctions": [], + "bodyKeys": [ + "name", + "type" + ], + "formatEN": "update group [name][type]", + "formatZH": "更新组 [name][type]", + "paramKeys": [] + } + } + }, + "/host/conffile/update": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "上传文件更新 SSH 配置", + "consumes": [ + "application/json" + ], + "tags": [ + "SSH" + ], + "summary": "Update host SSH setting by file", + "parameters": [ + { + "description": "request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.SSHConf" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + }, + "x-panel-log": { + "BeforeFunctions": [], + "bodyKeys": [], + "formatEN": "update SSH conf", + "formatZH": "修改 SSH 配置文件", + "paramKeys": [] + } + } + }, + "/host/ssh/conf": { + "get": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "获取 SSH 配置文件", + "tags": [ + "SSH" + ], + "summary": "Load host SSH conf", + "responses": { + "200": { + "description": "OK" + } + } + } + }, + "/host/ssh/generate": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "生成 SSH 密钥", + "consumes": [ + "application/json" + ], + "tags": [ + "SSH" + ], + "summary": "Generate host SSH secret", + "parameters": [ + { + "description": "request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.GenerateSSH" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + }, + "x-panel-log": { + "BeforeFunctions": [], + "bodyKeys": [], + "formatEN": "generate SSH secret", + "formatZH": "生成 SSH 密钥 ", + "paramKeys": [] + } + } + }, + "/host/ssh/log": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "获取 SSH 登录日志", + "consumes": [ + "application/json" + ], + "tags": [ + "SSH" + ], + "summary": "Load host SSH logs", + "parameters": [ + { + "description": "request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.SearchSSHLog" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/dto.SSHLog" + } + } + } + } + }, + "/host/ssh/operate": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "修改 SSH 服务状态", + "consumes": [ + "application/json" + ], + "tags": [ + "SSH" + ], + "summary": "Operate SSH", + "parameters": [ + { + "description": "request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.Operate" + } + } + ], + "responses": {}, + "x-panel-log": { + "BeforeFunctions": [], + "bodyKeys": [ + "operation" + ], + "formatEN": "[operation] SSH", + "formatZH": "[operation] SSH ", + "paramKeys": [] + } + } + }, + "/host/ssh/search": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "加载 SSH 配置信息", + "tags": [ + "SSH" + ], + "summary": "Load host SSH setting info", + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/dto.SSHInfo" + } + } + } + } + }, + "/host/ssh/secret": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "获取 SSH 密钥", + "consumes": [ + "application/json" + ], + "tags": [ + "SSH" + ], + "summary": "Load host SSH secret", + "parameters": [ + { + "description": "request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.GenerateLoad" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + } + } + }, + "/host/ssh/update": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "更新 SSH 配置", + "consumes": [ + "application/json" + ], + "tags": [ + "SSH" + ], + "summary": "Update host SSH setting", + "parameters": [ + { + "description": "request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.SSHUpdate" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + }, + "x-panel-log": { + "BeforeFunctions": [], + "bodyKeys": [ + "key", + "value" + ], + "formatEN": "update SSH setting [key] =\u003e [value]", + "formatZH": "修改 SSH 配置 [key] =\u003e [value]", + "paramKeys": [] + } + } + }, + "/host/tool": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "获取主机工具状态", + "consumes": [ + "application/json" + ], + "tags": [ + "Host tool" + ], + "summary": "Get tool", + "parameters": [ + { + "description": "request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/request.HostToolReq" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + } + } + }, + "/host/tool/config": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "操作主机工具配置文件", + "consumes": [ + "application/json" + ], + "tags": [ + "Host tool" + ], + "summary": "Get tool config", + "parameters": [ + { + "description": "request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/request.HostToolConfig" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + }, + "x-panel-log": { + "BeforeFunctions": [], + "bodyKeys": [ + "operate" + ], + "formatEN": "[operate] tool config", + "formatZH": "[operate] 主机工具配置文件 ", + "paramKeys": [] + } + } + }, + "/host/tool/create": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "创建主机工具配置", + "consumes": [ + "application/json" + ], + "tags": [ + "Host tool" + ], + "summary": "Create Host tool Config", + "parameters": [ + { + "description": "request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/request.HostToolCreate" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + }, + "x-panel-log": { + "BeforeFunctions": [], + "bodyKeys": [ + "type" + ], + "formatEN": "create [type] config", + "formatZH": "创建 [type] 配置", + "paramKeys": [] + } + } + }, + "/host/tool/log": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "获取主机工具日志", + "consumes": [ + "application/json" + ], + "tags": [ + "Host tool" + ], + "summary": "Get tool", + "parameters": [ + { + "description": "request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/request.HostToolLogReq" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + } + } + }, + "/host/tool/operate": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "操作主机工具", + "consumes": [ + "application/json" + ], + "tags": [ + "Host tool" + ], + "summary": "Operate tool", + "parameters": [ + { + "description": "request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/request.HostToolReq" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + }, + "x-panel-log": { + "BeforeFunctions": [], + "bodyKeys": [ + "operate", + "type" + ], + "formatEN": "[operate] [type]", + "formatZH": "[operate] [type] ", + "paramKeys": [] + } + } + }, + "/host/tool/supervisor/process": { + "get": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "获取 Supervisor 进程配置", + "consumes": [ + "application/json" + ], + "tags": [ + "Host tool" + ], + "summary": "Get Supervisor process config", + "responses": { + "200": { + "description": "OK" + } + } + }, + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "操作守护进程", + "consumes": [ + "application/json" + ], + "tags": [ + "Host tool" + ], + "summary": "Create Supervisor process", + "parameters": [ + { + "description": "request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/request.SupervisorProcessConfig" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + }, + "x-panel-log": { + "BeforeFunctions": [], + "bodyKeys": [ + "operate" + ], + "formatEN": "[operate] process", + "formatZH": "[operate] 守护进程 ", + "paramKeys": [] + } + } + }, + "/host/tool/supervisor/process/file": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "操作 Supervisor 进程文件", + "consumes": [ + "application/json" + ], + "tags": [ + "Host tool" + ], + "summary": "Get Supervisor process config", + "parameters": [ + { + "description": "request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/request.SupervisorProcessFileReq" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + }, + "x-panel-log": { + "BeforeFunctions": [], + "bodyKeys": [ + "operate" + ], + "formatEN": "[operate] Supervisor Process Config file", + "formatZH": "[operate] Supervisor 进程文件 ", + "paramKeys": [] + } + } + }, + "/hosts": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "创建主机", + "consumes": [ + "application/json" + ], + "tags": [ + "Host" + ], + "summary": "Create host", + "parameters": [ + { + "description": "request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.HostOperate" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + }, + "x-panel-log": { + "BeforeFunctions": [], + "bodyKeys": [ + "name", + "addr" + ], + "formatEN": "create host [name][addr]", + "formatZH": "创建主机 [name][addr]", + "paramKeys": [] + } + } + }, + "/hosts/command": { + "get": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "获取快速命令列表", + "tags": [ + "Command" + ], + "summary": "List commands", + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/dto.CommandInfo" + } + } + } + }, + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "创建快速命令", + "consumes": [ + "application/json" + ], + "tags": [ + "Command" + ], + "summary": "Create command", + "parameters": [ + { + "description": "request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.CommandOperate" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + }, + "x-panel-log": { + "BeforeFunctions": [], + "bodyKeys": [ + "name", + "command" + ], + "formatEN": "create quick command [name][command]", + "formatZH": "创建快捷命令 [name][command]", + "paramKeys": [] + } + } + }, + "/hosts/command/del": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "删除快速命令", + "consumes": [ + "application/json" + ], + "tags": [ + "Command" + ], + "summary": "Delete command", + "parameters": [ + { + "description": "request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.BatchDeleteReq" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + }, + "x-panel-log": { + "BeforeFunctions": [ + { + "db": "commands", + "input_column": "id", + "input_value": "ids", + "isList": true, + "output_column": "name", + "output_value": "names" + } + ], + "bodyKeys": [ + "ids" + ], + "formatEN": "delete quick command [names]", + "formatZH": "删除快捷命令 [names]", + "paramKeys": [] + } + } + }, + "/hosts/command/redis": { + "get": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "获取 redis 快速命令列表", + "tags": [ + "Redis Command" + ], + "summary": "List redis commands", + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "Array" + } + } + } + }, + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "保存 Redis 快速命令", + "consumes": [ + "application/json" + ], + "tags": [ + "Redis Command" + ], + "summary": "Save redis command", + "parameters": [ + { + "description": "request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.RedisCommand" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + }, + "x-panel-log": { + "BeforeFunctions": [], + "bodyKeys": [ + "name", + "command" + ], + "formatEN": "save quick command for redis [name][command]", + "formatZH": "保存 redis 快捷命令 [name][command]", + "paramKeys": [] + } + } + }, + "/hosts/command/redis/del": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "删除 redis 快速命令", + "consumes": [ + "application/json" + ], + "tags": [ + "Redis Command" + ], + "summary": "Delete redis command", + "parameters": [ + { + "description": "request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.BatchDeleteReq" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + }, + "x-panel-log": { + "BeforeFunctions": [ + { + "db": "redis_commands", + "input_column": "id", + "input_value": "ids", + "isList": true, + "output_column": "name", + "output_value": "names" + } + ], + "bodyKeys": [ + "ids" + ], + "formatEN": "delete quick command of redis [names]", + "formatZH": "删除 redis 快捷命令 [names]", + "paramKeys": [] + } + } + }, + "/hosts/command/redis/search": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "获取 redis 快速命令列表分页", + "consumes": [ + "application/json" + ], + "tags": [ + "Redis Command" + ], + "summary": "Page redis commands", + "parameters": [ + { + "description": "request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.SearchWithPage" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/dto.PageResult" + } + } + } + } + }, + "/hosts/command/search": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "获取快速命令列表分页", + "consumes": [ + "application/json" + ], + "tags": [ + "Command" + ], + "summary": "Page commands", + "parameters": [ + { + "description": "request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.SearchWithPage" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/dto.PageResult" + } + } + } + } + }, + "/hosts/command/tree": { + "get": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "获取快速命令树", + "consumes": [ + "application/json" + ], + "tags": [ + "Command" + ], + "summary": "Tree commands", + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "Array" + } + } + } + } + }, + "/hosts/command/update": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "更新快速命令", + "consumes": [ + "application/json" + ], + "tags": [ + "Command" + ], + "summary": "Update command", + "parameters": [ + { + "description": "request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.CommandOperate" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + }, + "x-panel-log": { + "BeforeFunctions": [], + "bodyKeys": [ + "name" + ], + "formatEN": "update quick command [name]", + "formatZH": "更新快捷命令 [name]", + "paramKeys": [] + } + } + }, + "/hosts/del": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "删除主机", + "consumes": [ + "application/json" + ], + "tags": [ + "Host" + ], + "summary": "Delete host", + "parameters": [ + { + "description": "request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.BatchDeleteReq" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + }, + "x-panel-log": { + "BeforeFunctions": [ + { + "db": "hosts", + "input_column": "id", + "input_value": "ids", + "isList": true, + "output_column": "addr", + "output_value": "addrs" + } + ], + "bodyKeys": [ + "ids" + ], + "formatEN": "delete host [addrs]", + "formatZH": "删除主机 [addrs]", + "paramKeys": [] + } + } + }, + "/hosts/firewall/base": { + "get": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "获取防火墙基础信息", + "tags": [ + "Firewall" + ], + "summary": "Load firewall base info", + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/dto.FirewallBaseInfo" + } + } + } + } + }, + "/hosts/firewall/batch": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "批量删除防火墙规则", + "consumes": [ + "application/json" + ], + "tags": [ + "Firewall" + ], + "summary": "Create group", + "parameters": [ + { + "description": "request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.BatchRuleOperate" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + } + } + }, + "/hosts/firewall/forward": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "更新防火墙端口转发规则", + "consumes": [ + "application/json" + ], + "tags": [ + "Firewall" + ], + "summary": "Create group", + "parameters": [ + { + "description": "request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.ForwardRuleOperate" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + }, + "x-panel-log": { + "BeforeFunctions": [], + "bodyKeys": [ + "source_port" + ], + "formatEN": "update port forward rules [source_port]", + "formatZH": "更新端口转发规则 [source_port]", + "paramKeys": [] + } + } + }, + "/hosts/firewall/ip": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "创建防火墙 IP 规则", + "consumes": [ + "application/json" + ], + "tags": [ + "Firewall" + ], + "summary": "Create group", + "parameters": [ + { + "description": "request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.AddrRuleOperate" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + }, + "x-panel-log": { + "BeforeFunctions": [], + "bodyKeys": [ + "strategy", + "address" + ], + "formatEN": "create address rules [strategy][address]", + "formatZH": "添加 ip 规则 [strategy] [address]", + "paramKeys": [] + } + } + }, + "/hosts/firewall/operate": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "修改防火墙状态", + "consumes": [ + "application/json" + ], + "tags": [ + "Firewall" + ], + "summary": "Page firewall status", + "parameters": [ + { + "description": "request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.FirewallOperation" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/dto.PageResult" + } + } + }, + "x-panel-log": { + "BeforeFunctions": [], + "bodyKeys": [ + "operation" + ], + "formatEN": "[operation] firewall", + "formatZH": "[operation] 防火墙", + "paramKeys": [] + } + } + }, + "/hosts/firewall/port": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "创建防火墙端口规则", + "consumes": [ + "application/json" + ], + "tags": [ + "Firewall" + ], + "summary": "Create group", + "parameters": [ + { + "description": "request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.PortRuleOperate" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + }, + "x-panel-log": { + "BeforeFunctions": [], + "bodyKeys": [ + "port", + "strategy" + ], + "formatEN": "create port rules [strategy][port]", + "formatZH": "添加端口规则 [strategy] [port]", + "paramKeys": [] + } + } + }, + "/hosts/firewall/search": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "获取防火墙规则列表分页", + "consumes": [ + "application/json" + ], + "tags": [ + "Firewall" + ], + "summary": "Page firewall rules", + "parameters": [ + { + "description": "request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.RuleSearch" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/dto.PageResult" + } + } + } + } + }, + "/hosts/firewall/update/addr": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "更新 ip 防火墙规则", + "consumes": [ + "application/json" + ], + "tags": [ + "Firewall" + ], + "summary": "Create group", + "parameters": [ + { + "description": "request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.AddrRuleUpdate" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + } + } + }, + "/hosts/firewall/update/description": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "更新防火墙描述", + "consumes": [ + "application/json" + ], + "tags": [ + "Firewall" + ], + "summary": "Update rule description", + "parameters": [ + { + "description": "request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.UpdateFirewallDescription" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + } + } + }, + "/hosts/firewall/update/port": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "更新端口防火墙规则", + "consumes": [ + "application/json" + ], + "tags": [ + "Firewall" + ], + "summary": "Create group", + "parameters": [ + { + "description": "request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.PortRuleUpdate" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + } + } + }, + "/hosts/monitor/clean": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "清空监控数据", + "tags": [ + "Monitor" + ], + "summary": "Clean monitor datas", + "responses": { + "200": { + "description": "OK" + } + }, + "x-panel-log": { + "BeforeFunctions": [], + "bodyKeys": [], + "formatEN": "clean monitor datas", + "formatZH": "清空监控数据", + "paramKeys": [] + } + } + }, + "/hosts/monitor/search": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "获取监控数据", + "tags": [ + "Monitor" + ], + "summary": "Load monitor datas", + "parameters": [ + { + "description": "request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.MonitorSearch" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + } + } + }, + "/hosts/search": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "获取主机列表分页", + "consumes": [ + "application/json" + ], + "tags": [ + "Host" + ], + "summary": "Page host", + "parameters": [ + { + "description": "request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.SearchHostWithPage" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/dto.HostTree" + } + } + } + } + } + }, + "/hosts/test/byid/:id": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "测试主机连接", + "consumes": [ + "application/json" + ], + "tags": [ + "Host" + ], + "summary": "Test host conn by host id", + "parameters": [ + { + "type": "integer", + "description": "request", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "boolean" + } + } + } + } + }, + "/hosts/test/byinfo": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "测试主机连接", + "consumes": [ + "application/json" + ], + "tags": [ + "Host" + ], + "summary": "Test host conn by info", + "parameters": [ + { + "description": "request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.HostConnTest" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + } + } + }, + "/hosts/tree": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "加载主机树", + "consumes": [ + "application/json" + ], + "tags": [ + "Host" + ], + "summary": "Load host tree", + "parameters": [ + { + "description": "request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.SearchForTree" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/dto.HostTree" + } + } + } + } + } + }, + "/hosts/update": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "更新主机", + "consumes": [ + "application/json" + ], + "tags": [ + "Host" + ], + "summary": "Update host", + "parameters": [ + { + "description": "request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.HostOperate" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + }, + "x-panel-log": { + "BeforeFunctions": [], + "bodyKeys": [ + "name", + "addr" + ], + "formatEN": "update host [name][addr]", + "formatZH": "更新主机信息 [name][addr]", + "paramKeys": [] + } + } + }, + "/hosts/update/group": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "切换分组", + "consumes": [ + "application/json" + ], + "tags": [ + "Host" + ], + "summary": "Update host group", + "parameters": [ + { + "description": "request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.ChangeHostGroup" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + }, + "x-panel-log": { + "BeforeFunctions": [ + { + "db": "hosts", + "input_column": "id", + "input_value": "id", + "isList": false, + "output_column": "addr", + "output_value": "addr" + } + ], + "bodyKeys": [ + "id", + "group" + ], + "formatEN": "change host [addr] group =\u003e [group]", + "formatZH": "切换主机[addr]分组 =\u003e [group]", + "paramKeys": [] + } + } + }, + "/logs/clean": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "清空操作日志", + "consumes": [ + "application/json" + ], + "tags": [ + "Logs" + ], + "summary": "Clean operation logs", + "parameters": [ + { + "description": "request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.CleanLog" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/dto.PageResult" + } + } + }, + "x-panel-log": { + "BeforeFunctions": [], + "bodyKeys": [ + "logType" + ], + "formatEN": "Clean the [logType] log information", + "formatZH": "清空 [logType] 日志信息", + "paramKeys": [] + } + } + }, + "/logs/login": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "获取系统登录日志列表分页", + "consumes": [ + "application/json" + ], + "tags": [ + "Logs" + ], + "summary": "Page login logs", + "parameters": [ + { + "description": "request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.SearchLgLogWithPage" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/dto.PageResult" + } + } + } + } + }, + "/logs/operation": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "获取系统操作日志列表分页", + "consumes": [ + "application/json" + ], + "tags": [ + "Logs" + ], + "summary": "Page operation logs", + "parameters": [ + { + "description": "request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.SearchOpLogWithPage" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/dto.PageResult" + } + } + } + } + }, + "/logs/system": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "获取系统日志", + "tags": [ + "Logs" + ], + "summary": "Load system logs", + "responses": { + "200": { + "description": "OK" + } + } + } + }, + "/logs/system/files": { + "get": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "获取系统日志文件列表", + "tags": [ + "Logs" + ], + "summary": "Load system log files", + "responses": { + "200": { + "description": "OK" + } + } + } + }, + "/openresty": { + "get": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "获取 OpenResty 配置信息", + "tags": [ + "OpenResty" + ], + "summary": "Load OpenResty conf", + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/response.FileInfo" + } + } + } + } + }, + "/openresty/clear": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "清理 OpenResty 代理缓存", + "tags": [ + "OpenResty" + ], + "summary": "Clear OpenResty proxy cache", + "responses": { + "200": { + "description": "OK" + } + }, + "x-panel-log": { + "BeforeFunctions": [], + "bodyKeys": [], + "formatEN": "Clear nginx proxy cache", + "formatZH": "清理 Openresty 代理缓存", + "paramKeys": [] + } + } + }, + "/openresty/file": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "上传更新 OpenResty 配置文件", + "consumes": [ + "application/json" + ], + "tags": [ + "OpenResty" + ], + "summary": "Update OpenResty conf by upload file", + "parameters": [ + { + "description": "request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/request.NginxConfigFileUpdate" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + }, + "x-panel-log": { + "BeforeFunctions": [], + "bodyKeys": [], + "formatEN": "Update nginx conf", + "formatZH": "更新 nginx 配置", + "paramKeys": [] + } + } + }, + "/openresty/scope": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "获取部分 OpenResty 配置信息", + "consumes": [ + "application/json" + ], + "tags": [ + "OpenResty" + ], + "summary": "Load partial OpenResty conf", + "parameters": [ + { + "description": "request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/request.NginxScopeReq" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/response.NginxParam" + } + } + } + } + } + }, + "/openresty/status": { + "get": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "获取 OpenResty 状态信息", + "tags": [ + "OpenResty" + ], + "summary": "Load OpenResty status info", + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/response.NginxStatus" + } + } + } + } + }, + "/openresty/update": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "更新 OpenResty 配置信息", + "consumes": [ + "application/json" + ], + "tags": [ + "OpenResty" + ], + "summary": "Update OpenResty conf", + "parameters": [ + { + "description": "request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/request.NginxConfigUpdate" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + }, + "x-panel-log": { + "BeforeFunctions": [ + { + "db": "websites", + "input_column": "id", + "input_value": "websiteId", + "isList": false, + "output_column": "primary_domain", + "output_value": "domain" + } + ], + "bodyKeys": [ + "websiteId" + ], + "formatEN": "Update nginx conf [domain]", + "formatZH": "更新 nginx 配置 [domain]", + "paramKeys": [] + } + } + }, + "/process/stop": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "停止进程", + "tags": [ + "Process" + ], + "summary": "Stop Process", + "parameters": [ + { + "description": "request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/request.ProcessReq" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + }, + "x-panel-log": { + "BeforeFunctions": [], + "bodyKeys": [ + "PID" + ], + "formatEN": "结束进程 [PID]", + "formatZH": "结束进程 [PID]", + "paramKeys": [] + } + } + }, + "/runtimes": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "创建运行环境", + "consumes": [ + "application/json" + ], + "tags": [ + "Runtime" + ], + "summary": "Create runtime", + "parameters": [ + { + "description": "request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/request.RuntimeCreate" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + }, + "x-panel-log": { + "BeforeFunctions": [], + "bodyKeys": [ + "name" + ], + "formatEN": "Create runtime [name]", + "formatZH": "创建运行环境 [name]", + "paramKeys": [] + } + } + }, + "/runtimes/:id": { + "get": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "获取运行环境", + "consumes": [ + "application/json" + ], + "tags": [ + "Runtime" + ], + "summary": "Get runtime", + "parameters": [ + { + "type": "string", + "description": "request", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK" + } + } + } + }, + "/runtimes/del": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "删除运行环境", + "consumes": [ + "application/json" + ], + "tags": [ + "Website" + ], + "summary": "Delete runtime", + "parameters": [ + { + "description": "request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/request.RuntimeDelete" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + }, + "x-panel-log": { + "BeforeFunctions": [], + "bodyKeys": [ + "id" + ], + "formatEN": "Delete website [name]", + "formatZH": "删除网站 [name]", + "paramKeys": [] + } + } + }, + "/runtimes/node/modules": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "获取 Node 项目的 modules", + "consumes": [ + "application/json" + ], + "tags": [ + "Runtime" + ], + "summary": "Get Node modules", + "parameters": [ + { + "description": "request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/request.NodeModuleReq" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + } + } + }, + "/runtimes/node/modules/operate": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "操作 Node 项目 modules", + "consumes": [ + "application/json" + ], + "tags": [ + "Runtime" + ], + "summary": "Operate Node modules", + "parameters": [ + { + "description": "request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/request.NodeModuleReq" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + } + } + }, + "/runtimes/node/package": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "获取 Node 项目的 scripts", + "consumes": [ + "application/json" + ], + "tags": [ + "Runtime" + ], + "summary": "Get Node package scripts", + "parameters": [ + { + "description": "request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/request.NodePackageReq" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + } + } + }, + "/runtimes/operate": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "操作运行环境", + "consumes": [ + "application/json" + ], + "tags": [ + "Runtime" + ], + "summary": "Operate runtime", + "parameters": [ + { + "description": "request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/request.RuntimeOperate" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + }, + "x-panel-log": { + "BeforeFunctions": [], + "bodyKeys": [ + "id" + ], + "formatEN": "Operate runtime [name]", + "formatZH": "操作运行环境 [name]", + "paramKeys": [] + } + } + }, + "/runtimes/php/extensions": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "Create Extensions", + "consumes": [ + "application/json" + ], + "tags": [ + "PHP Extensions" + ], + "summary": "Create Extensions", + "parameters": [ + { + "description": "request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/request.PHPExtensionsCreate" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + } + } + }, + "/runtimes/php/extensions/del": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "Delete Extensions", + "consumes": [ + "application/json" + ], + "tags": [ + "PHP Extensions" + ], + "summary": "Delete Extensions", + "parameters": [ + { + "description": "request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/request.PHPExtensionsDelete" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + } + } + }, + "/runtimes/php/extensions/search": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "Page Extensions", + "consumes": [ + "application/json" + ], + "tags": [ + "PHP Extensions" + ], + "summary": "Page Extensions", + "parameters": [ + { + "description": "request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/request.PHPExtensionsSearch" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/response.PHPExtensionsDTO" + } + } + } + } + } + }, + "/runtimes/php/extensions/update": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "Update Extensions", + "consumes": [ + "application/json" + ], + "tags": [ + "PHP Extensions" + ], + "summary": "Update Extensions", + "parameters": [ + { + "description": "request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/request.PHPExtensionsUpdate" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + } + } + }, + "/runtimes/search": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "获取运行环境列表", + "consumes": [ + "application/json" + ], + "tags": [ + "Runtime" + ], + "summary": "List runtimes", + "parameters": [ + { + "description": "request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/request.RuntimeSearch" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + } + } + }, + "/runtimes/sync": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "同步运行环境状态", + "consumes": [ + "application/json" + ], + "tags": [ + "Runtime" + ], + "summary": "Sync runtime status", + "responses": { + "200": { + "description": "OK" + } + } + } + }, + "/runtimes/update": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "更新运行环境", + "consumes": [ + "application/json" + ], + "tags": [ + "Runtime" + ], + "summary": "Update runtime", + "parameters": [ + { + "description": "request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/request.RuntimeUpdate" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + }, + "x-panel-log": { + "BeforeFunctions": [], + "bodyKeys": [ + "name" + ], + "formatEN": "Update runtime [name]", + "formatZH": "更新运行环境 [name]", + "paramKeys": [] + } + } + }, + "/settings/backup": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "创建备份账号", + "consumes": [ + "application/json" + ], + "tags": [ + "Backup Account" + ], + "summary": "Create backup account", + "parameters": [ + { + "description": "request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.BackupOperate" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + }, + "x-panel-log": { + "BeforeFunctions": [], + "bodyKeys": [ + "type" + ], + "formatEN": "create backup account [type]", + "formatZH": "创建备份账号 [type]", + "paramKeys": [] + } + } + }, + "/settings/backup/backup": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "备份系统数据", + "consumes": [ + "application/json" + ], + "tags": [ + "Backup Account" + ], + "summary": "Backup system data", + "parameters": [ + { + "description": "request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.CommonBackup" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + }, + "x-panel-log": { + "BeforeFunctions": [], + "bodyKeys": [ + "type", + "name", + "detailName" + ], + "formatEN": "backup [type] data [name][detailName]", + "formatZH": "备份 [type] 数据 [name][detailName]", + "paramKeys": [] + } + } + }, + "/settings/backup/del": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "删除备份账号", + "consumes": [ + "application/json" + ], + "tags": [ + "Backup Account" + ], + "summary": "Delete backup account", + "parameters": [ + { + "description": "request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.OperateByID" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + }, + "x-panel-log": { + "BeforeFunctions": [ + { + "db": "backup_accounts", + "input_column": "id", + "input_value": "id", + "isList": false, + "output_column": "type", + "output_value": "types" + } + ], + "bodyKeys": [ + "id" + ], + "formatEN": "delete backup account [types]", + "formatZH": "删除备份账号 [types]", + "paramKeys": [] + } + } + }, + "/settings/backup/onedrive": { + "get": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "获取 OneDrive 信息", + "consumes": [ + "application/json" + ], + "tags": [ + "Backup Account" + ], + "summary": "Load OneDrive info", + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/dto.OneDriveInfo" + } + } + } + } + }, + "/settings/backup/record/del": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "删除备份记录", + "consumes": [ + "application/json" + ], + "tags": [ + "Backup Account" + ], + "summary": "Delete backup record", + "parameters": [ + { + "description": "request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.BatchDeleteReq" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + }, + "x-panel-log": { + "BeforeFunctions": [ + { + "db": "backup_records", + "input_column": "id", + "input_value": "ids", + "isList": true, + "output_column": "file_name", + "output_value": "files" + } + ], + "bodyKeys": [ + "ids" + ], + "formatEN": "delete backup records [files]", + "formatZH": "删除备份记录 [files]", + "paramKeys": [] + } + } + }, + "/settings/backup/record/download": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "下载备份记录", + "consumes": [ + "application/json" + ], + "tags": [ + "Backup Account" + ], + "summary": "Download backup record", + "parameters": [ + { + "description": "request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.DownloadRecord" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + }, + "x-panel-log": { + "BeforeFunctions": [], + "bodyKeys": [ + "source", + "fileName" + ], + "formatEN": "download backup records [source][fileName]", + "formatZH": "下载备份记录 [source][fileName]", + "paramKeys": [] + } + } + }, + "/settings/backup/record/search": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "获取备份记录列表分页", + "consumes": [ + "application/json" + ], + "tags": [ + "Backup Account" + ], + "summary": "Page backup records", + "parameters": [ + { + "description": "request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.RecordSearch" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + } + } + }, + "/settings/backup/record/search/bycronjob": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "通过计划任务获取备份记录列表分页", + "consumes": [ + "application/json" + ], + "tags": [ + "Backup Account" + ], + "summary": "Page backup records by cronjob", + "parameters": [ + { + "description": "request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.RecordSearchByCronjob" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + } + } + }, + "/settings/backup/recover": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "恢复系统数据", + "consumes": [ + "application/json" + ], + "tags": [ + "Backup Account" + ], + "summary": "Recover system data", + "parameters": [ + { + "description": "request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.CommonRecover" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + }, + "x-panel-log": { + "BeforeFunctions": [], + "bodyKeys": [ + "type", + "name", + "detailName", + "file" + ], + "formatEN": "recover [type] data [name][detailName] from [file]", + "formatZH": "从 [file] 恢复 [type] 数据 [name][detailName]", + "paramKeys": [] + } + } + }, + "/settings/backup/recover/byupload": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "从上传恢复系统数据", + "consumes": [ + "application/json" + ], + "tags": [ + "Backup Account" + ], + "summary": "Recover system data by upload", + "parameters": [ + { + "description": "request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.CommonRecover" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + }, + "x-panel-log": { + "BeforeFunctions": [], + "bodyKeys": [ + "type", + "name", + "detailName", + "file" + ], + "formatEN": "recover [type] data [name][detailName] from [file]", + "formatZH": "从 [file] 恢复 [type] 数据 [name][detailName]", + "paramKeys": [] + } + } + }, + "/settings/backup/refresh/onedrive": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "刷新 OneDrive token", + "tags": [ + "Backup Account" + ], + "summary": "Refresh OneDrive token", + "responses": { + "200": { + "description": "OK" + } + } + } + }, + "/settings/backup/search": { + "get": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "获取备份账号列表", + "tags": [ + "Backup Account" + ], + "summary": "List backup accounts", + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/dto.BackupInfo" + } + } + } + } + }, + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "获取 bucket 列表", + "consumes": [ + "application/json" + ], + "tags": [ + "Backup Account" + ], + "summary": "List buckets", + "parameters": [ + { + "description": "request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.ForBuckets" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "array", + "items": { + "type": "string" + } + } + } + } + } + }, + "/settings/backup/search/files": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "获取备份账号内文件列表", + "consumes": [ + "application/json" + ], + "tags": [ + "Backup Account" + ], + "summary": "List files from backup accounts", + "parameters": [ + { + "description": "request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.BackupSearchFile" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "array", + "items": { + "type": "string" + } + } + } + } + } + }, + "/settings/backup/update": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "更新备份账号信息", + "consumes": [ + "application/json" + ], + "tags": [ + "Backup Account" + ], + "summary": "Update backup account", + "parameters": [ + { + "description": "request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.BackupOperate" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + }, + "x-panel-log": { + "BeforeFunctions": [], + "bodyKeys": [ + "type" + ], + "formatEN": "update backup account [types]", + "formatZH": "更新备份账号 [types]", + "paramKeys": [] + } + } + }, + "/settings/basedir": { + "get": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "获取安装根目录", + "tags": [ + "System Setting" + ], + "summary": "Load local backup dir", + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "string" + } + } + } + } + }, + "/settings/bind/update": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "更新系统监听信息", + "consumes": [ + "application/json" + ], + "tags": [ + "System Setting" + ], + "summary": "Update system bind info", + "parameters": [ + { + "description": "request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.BindInfo" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + }, + "x-panel-log": { + "BeforeFunctions": [], + "bodyKeys": [ + "ipv6", + "bindAddress" + ], + "formatEN": "update system bind info =\u003e ipv6: [ipv6], 监听 IP: [bindAddress]", + "formatZH": "修改系统监听信息 =\u003e ipv6: [ipv6], 监听 IP: [bindAddress]", + "paramKeys": [] + } + } + }, + "/settings/expired/handle": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "重置过期系统登录密码", + "consumes": [ + "application/json" + ], + "tags": [ + "System Setting" + ], + "summary": "Reset system password expired", + "parameters": [ + { + "description": "request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.PasswordUpdate" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + }, + "x-panel-log": { + "BeforeFunctions": [], + "bodyKeys": [], + "formatEN": "reset an expired Password", + "formatZH": "重置过期密码", + "paramKeys": [] + } + } + }, + "/settings/interface": { + "get": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "获取系统地址信息", + "consumes": [ + "application/json" + ], + "tags": [ + "System Setting" + ], + "summary": "Load system address", + "responses": { + "200": { + "description": "OK" + } + } + } + }, + "/settings/menu/update": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "隐藏高级功能菜单", + "consumes": [ + "application/json" + ], + "tags": [ + "System Setting" + ], + "summary": "Update system setting", + "parameters": [ + { + "description": "request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.SettingUpdate" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + }, + "x-panel-log": { + "BeforeFunctions": [], + "bodyKeys": [], + "formatEN": "Hide advanced feature menu.", + "formatZH": "隐藏高级功能菜单", + "paramKeys": [] + } + } + }, + "/settings/mfa": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "获取 mfa 信息", + "consumes": [ + "application/json" + ], + "tags": [ + "System Setting" + ], + "summary": "Load mfa info", + "parameters": [ + { + "description": "request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.MfaCredential" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/mfa.Otp" + } + } + } + } + }, + "/settings/mfa/bind": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "Mfa 绑定", + "consumes": [ + "application/json" + ], + "tags": [ + "System Setting" + ], + "summary": "Bind mfa", + "parameters": [ + { + "description": "request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.MfaCredential" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + }, + "x-panel-log": { + "BeforeFunctions": [], + "bodyKeys": [], + "formatEN": "bind mfa", + "formatZH": "mfa 绑定", + "paramKeys": [] + } + } + }, + "/settings/password/update": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "更新系统登录密码", + "consumes": [ + "application/json" + ], + "tags": [ + "System Setting" + ], + "summary": "Update system password", + "parameters": [ + { + "description": "request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.PasswordUpdate" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + }, + "x-panel-log": { + "BeforeFunctions": [], + "bodyKeys": [], + "formatEN": "update system password", + "formatZH": "修改系统密码", + "paramKeys": [] + } + } + }, + "/settings/port/update": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "更新系统端口", + "consumes": [ + "application/json" + ], + "tags": [ + "System Setting" + ], + "summary": "Update system port", + "parameters": [ + { + "description": "request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.PortUpdate" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + }, + "x-panel-log": { + "BeforeFunctions": [], + "bodyKeys": [ + "serverPort" + ], + "formatEN": "update system port =\u003e [serverPort]", + "formatZH": "修改系统端口 =\u003e [serverPort]", + "paramKeys": [] + } + } + }, + "/settings/proxy/update": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "服务器代理配置", + "consumes": [ + "application/json" + ], + "tags": [ + "System Setting" + ], + "summary": "Update proxy setting", + "parameters": [ + { + "description": "request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.ProxyUpdate" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + }, + "x-panel-log": { + "BeforeFunctions": [], + "bodyKeys": [ + "proxyUrl", + "proxyPort" + ], + "formatEN": "set proxy [proxyPort]:[proxyPort].", + "formatZH": "服务器代理配置 [proxyPort]:[proxyPort]", + "paramKeys": [] + } + } + }, + "/settings/search": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "加载系统配置信息", + "tags": [ + "System Setting" + ], + "summary": "Load system setting info", + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/dto.SettingInfo" + } + } + } + } + }, + "/settings/search/available": { + "get": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "获取系统可用状态", + "tags": [ + "System Setting" + ], + "summary": "Load system available status", + "responses": { + "200": { + "description": "OK" + } + } + } + }, + "/settings/snapshot": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "创建系统快照", + "consumes": [ + "application/json" + ], + "tags": [ + "System Setting" + ], + "summary": "Create system snapshot", + "parameters": [ + { + "description": "request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.SnapshotCreate" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + }, + "x-panel-log": { + "BeforeFunctions": [], + "bodyKeys": [ + "from", + "description" + ], + "formatEN": "Create system backup [description] to [from]", + "formatZH": "创建系统快照 [description] 到 [from]", + "paramKeys": [] + } + } + }, + "/settings/snapshot/del": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "删除系统快照", + "consumes": [ + "application/json" + ], + "tags": [ + "System Setting" + ], + "summary": "Delete system backup", + "parameters": [ + { + "description": "request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.SnapshotBatchDelete" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + }, + "x-panel-log": { + "BeforeFunctions": [ + { + "db": "snapshots", + "input_column": "id", + "input_value": "ids", + "isList": true, + "output_column": "name", + "output_value": "name" + } + ], + "bodyKeys": [ + "ids" + ], + "formatEN": "Delete system backup [name]", + "formatZH": "删除系统快照 [name]", + "paramKeys": [] + } + } + }, + "/settings/snapshot/description/update": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "更新快照描述信息", + "consumes": [ + "application/json" + ], + "tags": [ + "System Setting" + ], + "summary": "Update snapshot description", + "parameters": [ + { + "description": "request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.UpdateDescription" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + }, + "x-panel-log": { + "BeforeFunctions": [ + { + "db": "snapshots", + "input_column": "id", + "input_value": "id", + "isList": false, + "output_column": "name", + "output_value": "name" + } + ], + "bodyKeys": [ + "id", + "description" + ], + "formatEN": "The description of the snapshot [name] is modified =\u003e [description]", + "formatZH": "快照 [name] 描述信息修改 [description]", + "paramKeys": [] + } + } + }, + "/settings/snapshot/import": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "导入已有快照", + "consumes": [ + "application/json" + ], + "tags": [ + "System Setting" + ], + "summary": "Import system snapshot", + "parameters": [ + { + "description": "request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.SnapshotImport" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + }, + "x-panel-log": { + "BeforeFunctions": [], + "bodyKeys": [ + "from", + "names" + ], + "formatEN": "Sync system snapshots [names] from [from]", + "formatZH": "从 [from] 同步系统快照 [names]", + "paramKeys": [] + } + } + }, + "/settings/snapshot/recover": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "从系统快照恢复", + "consumes": [ + "application/json" + ], + "tags": [ + "System Setting" + ], + "summary": "Recover system backup", + "parameters": [ + { + "description": "request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.SnapshotRecover" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + }, + "x-panel-log": { + "BeforeFunctions": [ + { + "db": "snapshots", + "input_column": "id", + "input_value": "id", + "isList": false, + "output_column": "name", + "output_value": "name" + } + ], + "bodyKeys": [ + "id" + ], + "formatEN": "Recover from system backup [name]", + "formatZH": "从系统快照 [name] 恢复", + "paramKeys": [] + } + } + }, + "/settings/snapshot/rollback": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "从系统快照回滚", + "consumes": [ + "application/json" + ], + "tags": [ + "System Setting" + ], + "summary": "Rollback system backup", + "parameters": [ + { + "description": "request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.SnapshotRecover" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + }, + "x-panel-log": { + "BeforeFunctions": [ + { + "db": "snapshots", + "input_column": "id", + "input_value": "id", + "isList": false, + "output_column": "name", + "output_value": "name" + } + ], + "bodyKeys": [ + "id" + ], + "formatEN": "Rollback from system backup [name]", + "formatZH": "从系统快照 [name] 回滚", + "paramKeys": [] + } + } + }, + "/settings/snapshot/search": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "获取系统快照列表分页", + "consumes": [ + "application/json" + ], + "tags": [ + "System Setting" + ], + "summary": "Page system snapshot", + "parameters": [ + { + "description": "request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.SearchWithPage" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/dto.PageResult" + } + } + } + } + }, + "/settings/snapshot/status": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "获取快照状态", + "consumes": [ + "application/json" + ], + "tags": [ + "System Setting" + ], + "summary": "Load Snapshot status", + "parameters": [ + { + "description": "request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.OperateByID" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + } + } + }, + "/settings/ssl/download": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "下载证书", + "tags": [ + "System Setting" + ], + "summary": "Download system cert", + "responses": { + "200": { + "description": "OK" + } + } + } + }, + "/settings/ssl/info": { + "get": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "获取证书信息", + "tags": [ + "System Setting" + ], + "summary": "Load system cert info", + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/dto.SettingInfo" + } + } + } + } + }, + "/settings/ssl/update": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "修改系统 ssl 登录", + "consumes": [ + "application/json" + ], + "tags": [ + "System Setting" + ], + "summary": "Update system ssl", + "parameters": [ + { + "description": "request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.SSLUpdate" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + }, + "x-panel-log": { + "BeforeFunctions": [], + "bodyKeys": [ + "ssl" + ], + "formatEN": "update system ssl =\u003e [ssl]", + "formatZH": "修改系统 ssl =\u003e [ssl]", + "paramKeys": [] + } + } + }, + "/settings/update": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "更新系统配置", + "consumes": [ + "application/json" + ], + "tags": [ + "System Setting" + ], + "summary": "Update system setting", + "parameters": [ + { + "description": "request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.SettingUpdate" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + }, + "x-panel-log": { + "BeforeFunctions": [], + "bodyKeys": [ + "key", + "value" + ], + "formatEN": "update system setting [key] =\u003e [value]", + "formatZH": "修改系统配置 [key] =\u003e [value]", + "paramKeys": [] + } + } + }, + "/settings/upgrade": { + "get": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "获取版本 release notes", + "consumes": [ + "application/json" + ], + "tags": [ + "System Setting" + ], + "summary": "Load release notes by version", + "parameters": [ + { + "description": "request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.Upgrade" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + } + }, + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "系统更新", + "consumes": [ + "application/json" + ], + "tags": [ + "System Setting" + ], + "summary": "Upgrade", + "parameters": [ + { + "description": "request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.Upgrade" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + }, + "x-panel-log": { + "BeforeFunctions": [], + "bodyKeys": [ + "version" + ], + "formatEN": "upgrade system =\u003e [version]", + "formatZH": "更新系统 =\u003e [version]", + "paramKeys": [] + } + } + }, + "/toolbox/clam": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "创建扫描规则", + "consumes": [ + "application/json" + ], + "tags": [ + "Clam" + ], + "summary": "Create clam", + "parameters": [ + { + "description": "request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.ClamCreate" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + }, + "x-panel-log": { + "BeforeFunctions": [], + "bodyKeys": [ + "name", + "path" + ], + "formatEN": "create clam [name][path]", + "formatZH": "创建扫描规则 [name][path]", + "paramKeys": [] + } + } + }, + "/toolbox/clam/base": { + "get": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "获取 Clam 基础信息", + "consumes": [ + "application/json" + ], + "tags": [ + "Clam" + ], + "summary": "Load clam base info", + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/dto.ClamBaseInfo" + } + } + } + } + }, + "/toolbox/clam/del": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "删除扫描规则", + "consumes": [ + "application/json" + ], + "tags": [ + "Clam" + ], + "summary": "Delete clam", + "parameters": [ + { + "description": "request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.ClamDelete" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + }, + "x-panel-log": { + "BeforeFunctions": [ + { + "db": "clams", + "input_column": "id", + "input_value": "ids", + "isList": true, + "output_column": "name", + "output_value": "names" + } + ], + "bodyKeys": [ + "ids" + ], + "formatEN": "delete clam [names]", + "formatZH": "删除扫描规则 [names]", + "paramKeys": [] + } + } + }, + "/toolbox/clam/file/search": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "获取扫描文件", + "consumes": [ + "application/json" + ], + "tags": [ + "Clam" + ], + "summary": "Load clam file", + "parameters": [ + { + "description": "request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.ClamFileReq" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/dto.PageResult" + } + } + } + } + }, + "/toolbox/clam/file/update": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "更新病毒扫描配置文件", + "consumes": [ + "application/json" + ], + "tags": [ + "Clam" + ], + "summary": "Update clam file", + "parameters": [ + { + "description": "request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.UpdateByNameAndFile" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + } + } + }, + "/toolbox/clam/handle": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "执行病毒扫描", + "consumes": [ + "application/json" + ], + "tags": [ + "Clam" + ], + "summary": "Handle clam scan", + "parameters": [ + { + "description": "request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.OperateByID" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + }, + "x-panel-log": { + "BeforeFunctions": [ + { + "db": "clams", + "input_column": "id", + "input_value": "id", + "isList": true, + "output_column": "name", + "output_value": "name" + } + ], + "bodyKeys": [ + "id" + ], + "formatEN": "handle clam scan [name]", + "formatZH": "执行病毒扫描 [name]", + "paramKeys": [] + } + } + }, + "/toolbox/clam/operate": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "修改 Clam 状态", + "consumes": [ + "application/json" + ], + "tags": [ + "Clam" + ], + "summary": "Operate Clam", + "parameters": [ + { + "description": "request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.Operate" + } + } + ], + "responses": {}, + "x-panel-log": { + "BeforeFunctions": [], + "bodyKeys": [ + "operation" + ], + "formatEN": "[operation] FTP", + "formatZH": "[operation] Clam", + "paramKeys": [] + } + } + }, + "/toolbox/clam/record/clean": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "清空扫描报告", + "consumes": [ + "application/json" + ], + "tags": [ + "Clam" + ], + "summary": "Clean clam record", + "parameters": [ + { + "description": "request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.OperateByID" + } + } + ], + "responses": {}, + "x-panel-log": { + "BeforeFunctions": [ + { + "db": "clams", + "input_column": "id", + "input_value": "id", + "isList": true, + "output_column": "name", + "output_value": "name" + } + ], + "bodyKeys": [ + "id" + ], + "formatEN": "clean clam record [name]", + "formatZH": "清空扫描报告 [name]", + "paramKeys": [] + } + } + }, + "/toolbox/clam/record/log": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "获取扫描结果详情", + "consumes": [ + "application/json" + ], + "tags": [ + "Clam" + ], + "summary": "Load clam record detail", + "parameters": [ + { + "description": "request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.ClamLogReq" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + } + } + }, + "/toolbox/clam/record/search": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "获取扫描结果列表分页", + "consumes": [ + "application/json" + ], + "tags": [ + "Clam" + ], + "summary": "Page clam record", + "parameters": [ + { + "description": "request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.ClamLogSearch" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/dto.PageResult" + } + } + } + } + }, + "/toolbox/clam/search": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "获取扫描规则列表分页", + "consumes": [ + "application/json" + ], + "tags": [ + "Clam" + ], + "summary": "Page clam", + "parameters": [ + { + "description": "request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.SearchWithPage" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/dto.PageResult" + } + } + } + } + }, + "/toolbox/clam/status/update": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "修改扫描规则状态", + "consumes": [ + "application/json" + ], + "tags": [ + "Clam" + ], + "summary": "Update clam status", + "parameters": [ + { + "description": "request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.ClamUpdateStatus" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + }, + "x-panel-log": { + "BeforeFunctions": [ + { + "db": "clams", + "input_column": "id", + "input_value": "id", + "isList": false, + "output_column": "name", + "output_value": "name" + } + ], + "bodyKeys": [ + "id", + "status" + ], + "formatEN": "change the status of clam [name] to [status].", + "formatZH": "修改扫描规则 [name] 状态为 [status]", + "paramKeys": [] + } + } + }, + "/toolbox/clam/update": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "修改扫描规则", + "consumes": [ + "application/json" + ], + "tags": [ + "Clam" + ], + "summary": "Update clam", + "parameters": [ + { + "description": "request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.ClamUpdate" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + }, + "x-panel-log": { + "BeforeFunctions": [], + "bodyKeys": [ + "name", + "path" + ], + "formatEN": "update clam [name][path]", + "formatZH": "修改扫描规则 [name][path]", + "paramKeys": [] + } + } + }, + "/toolbox/clean": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "清理系统垃圾文件", + "consumes": [ + "application/json" + ], + "tags": [ + "Device" + ], + "summary": "Clean system", + "parameters": [ + { + "description": "request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/dto.Clean" + } + } + } + ], + "responses": { + "200": { + "description": "OK" + } + }, + "x-panel-log": { + "BeforeFunctions": [], + "bodyKeys": [], + "formatEN": "Clean system junk files", + "formatZH": "清理系统垃圾文件", + "paramKeys": [] + } + } + }, + "/toolbox/device/base": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "获取设备基础信息", + "tags": [ + "Device" + ], + "summary": "Load device base info", + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/dto.DeviceBaseInfo" + } + } + } + } + }, + "/toolbox/device/check/dns": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "检查系统 DNS 配置可用性", + "consumes": [ + "application/json" + ], + "tags": [ + "Device" + ], + "summary": "Check device DNS conf", + "parameters": [ + { + "description": "request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.SettingUpdate" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + } + } + }, + "/toolbox/device/conf": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "获取系统配置文件", + "consumes": [ + "application/json" + ], + "tags": [ + "Device" + ], + "summary": "load conf", + "parameters": [ + { + "description": "request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.OperationWithName" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + } + } + }, + "/toolbox/device/update/byconf": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "通过文件修改配置", + "consumes": [ + "application/json" + ], + "tags": [ + "Device" + ], + "summary": "Update device conf by file", + "parameters": [ + { + "description": "request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.UpdateByNameAndFile" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + } + } + }, + "/toolbox/device/update/conf": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "修改系统参数", + "consumes": [ + "application/json" + ], + "tags": [ + "Device" + ], + "summary": "Update device", + "parameters": [ + { + "description": "request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.SettingUpdate" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + }, + "x-panel-log": { + "BeforeFunctions": [], + "bodyKeys": [ + "key", + "value" + ], + "formatEN": "update device conf [key] =\u003e [value]", + "formatZH": "修改主机参数 [key] =\u003e [value]", + "paramKeys": [] + } + } + }, + "/toolbox/device/update/host": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "修改系统 hosts", + "tags": [ + "Device" + ], + "summary": "Update device hosts", + "responses": { + "200": { + "description": "OK" + } + }, + "x-panel-log": { + "BeforeFunctions": [], + "bodyKeys": [ + "key", + "value" + ], + "formatEN": "update device host [key] =\u003e [value]", + "formatZH": "修改主机 Host [key] =\u003e [value]", + "paramKeys": [] + } + } + }, + "/toolbox/device/update/passwd": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "修改系统密码", + "consumes": [ + "application/json" + ], + "tags": [ + "Device" + ], + "summary": "Update device passwd", + "parameters": [ + { + "description": "request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.ChangePasswd" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + } + } + }, + "/toolbox/device/update/swap": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "修改系统 Swap", + "consumes": [ + "application/json" + ], + "tags": [ + "Device" + ], + "summary": "Update device swap", + "parameters": [ + { + "description": "request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.SwapHelper" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + }, + "x-panel-log": { + "BeforeFunctions": [], + "bodyKeys": [ + "operate", + "path" + ], + "formatEN": "[operate] device swap [path]", + "formatZH": "[operate] 主机 swap [path]", + "paramKeys": [] + } + } + }, + "/toolbox/device/zone/options": { + "get": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "获取系统可用时区选项", + "consumes": [ + "application/json" + ], + "tags": [ + "Device" + ], + "summary": "list time zone options", + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "Array" + } + } + } + } + }, + "/toolbox/fail2ban/base": { + "get": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "获取 Fail2ban 基础信息", + "tags": [ + "Fail2ban" + ], + "summary": "Load fail2ban base info", + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/dto.Fail2BanBaseInfo" + } + } + } + } + }, + "/toolbox/fail2ban/load/conf": { + "get": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "获取 fail2ban 配置文件", + "consumes": [ + "application/json" + ], + "tags": [ + "Fail2ban" + ], + "summary": "Load fail2ban conf", + "responses": { + "200": { + "description": "OK" + } + } + } + }, + "/toolbox/fail2ban/operate": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "修改 Fail2ban 状态", + "consumes": [ + "application/json" + ], + "tags": [ + "Fail2ban" + ], + "summary": "Operate fail2ban", + "parameters": [ + { + "description": "request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.Operate" + } + } + ], + "responses": {}, + "x-panel-log": { + "BeforeFunctions": [], + "bodyKeys": [ + "operation" + ], + "formatEN": "[operation] Fail2ban", + "formatZH": "[operation] Fail2ban", + "paramKeys": [] + } + } + }, + "/toolbox/fail2ban/operate/sshd": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "配置 sshd", + "consumes": [ + "application/json" + ], + "tags": [ + "Fail2ban" + ], + "summary": "Operate sshd of fail2ban", + "parameters": [ + { + "description": "request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.Operate" + } + } + ], + "responses": {} + } + }, + "/toolbox/fail2ban/search": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "获取 Fail2ban ip", + "consumes": [ + "application/json" + ], + "tags": [ + "Fail2ban" + ], + "summary": "Page fail2ban ip list", + "parameters": [ + { + "description": "request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.Fail2BanSearch" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "Array" + } + } + } + } + }, + "/toolbox/fail2ban/update": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "修改 Fail2ban 配置", + "consumes": [ + "application/json" + ], + "tags": [ + "Fail2ban" + ], + "summary": "Update fail2ban conf", + "parameters": [ + { + "description": "request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.Fail2BanUpdate" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + }, + "x-panel-log": { + "BeforeFunctions": [], + "bodyKeys": [ + "key", + "value" + ], + "formatEN": "update fail2ban conf [key] =\u003e [value]", + "formatZH": "修改 Fail2ban 配置 [key] =\u003e [value]", + "paramKeys": [] + } + } + }, + "/toolbox/fail2ban/update/byconf": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "通过文件修改 fail2ban 配置", + "consumes": [ + "application/json" + ], + "tags": [ + "Fail2ban" + ], + "summary": "Update fail2ban conf by file", + "parameters": [ + { + "description": "request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.UpdateByFile" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + } + } + }, + "/toolbox/ftp": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "创建 FTP 账户", + "consumes": [ + "application/json" + ], + "tags": [ + "FTP" + ], + "summary": "Create FTP user", + "parameters": [ + { + "description": "request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.FtpCreate" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + }, + "x-panel-log": { + "BeforeFunctions": [], + "bodyKeys": [ + "user", + "path" + ], + "formatEN": "create FTP [user][path]", + "formatZH": "创建 FTP 账户 [user][path]", + "paramKeys": [] + } + } + }, + "/toolbox/ftp/base": { + "get": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "获取 FTP 基础信息", + "tags": [ + "FTP" + ], + "summary": "Load FTP base info", + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/dto.FtpBaseInfo" + } + } + } + } + }, + "/toolbox/ftp/del": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "删除 FTP 账户", + "consumes": [ + "application/json" + ], + "tags": [ + "FTP" + ], + "summary": "Delete FTP user", + "parameters": [ + { + "description": "request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.BatchDeleteReq" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + }, + "x-panel-log": { + "BeforeFunctions": [ + { + "db": "ftps", + "input_column": "id", + "input_value": "ids", + "isList": true, + "output_column": "user", + "output_value": "users" + } + ], + "bodyKeys": [ + "ids" + ], + "formatEN": "delete FTP users [users]", + "formatZH": "删除 FTP 账户 [users]", + "paramKeys": [] + } + } + }, + "/toolbox/ftp/log/search": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "获取 FTP 操作日志", + "consumes": [ + "application/json" + ], + "tags": [ + "FTP" + ], + "summary": "Load FTP operation log", + "parameters": [ + { + "description": "request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.FtpLogSearch" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/dto.PageResult" + } + } + } + } + }, + "/toolbox/ftp/operate": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "修改 FTP 状态", + "consumes": [ + "application/json" + ], + "tags": [ + "FTP" + ], + "summary": "Operate FTP", + "parameters": [ + { + "description": "request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.Operate" + } + } + ], + "responses": {}, + "x-panel-log": { + "BeforeFunctions": [], + "bodyKeys": [ + "operation" + ], + "formatEN": "[operation] FTP", + "formatZH": "[operation] FTP", + "paramKeys": [] + } + } + }, + "/toolbox/ftp/search": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "获取 FTP 账户列表分页", + "consumes": [ + "application/json" + ], + "tags": [ + "FTP" + ], + "summary": "Page FTP user", + "parameters": [ + { + "description": "request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.SearchWithPage" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/dto.PageResult" + } + } + } + } + }, + "/toolbox/ftp/sync": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "同步 FTP 账户", + "consumes": [ + "application/json" + ], + "tags": [ + "FTP" + ], + "summary": "Sync FTP user", + "parameters": [ + { + "description": "request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.BatchDeleteReq" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + }, + "x-panel-log": { + "BeforeFunctions": [], + "bodyKeys": [], + "formatEN": "sync FTP users", + "formatZH": "同步 FTP 账户", + "paramKeys": [] + } + } + }, + "/toolbox/ftp/update": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "修改 FTP 账户", + "consumes": [ + "application/json" + ], + "tags": [ + "FTP" + ], + "summary": "Update FTP user", + "parameters": [ + { + "description": "request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.FtpUpdate" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + }, + "x-panel-log": { + "BeforeFunctions": [], + "bodyKeys": [ + "user", + "path" + ], + "formatEN": "update FTP [user][path]", + "formatZH": "修改 FTP 账户 [user][path]", + "paramKeys": [] + } + } + }, + "/toolbox/scan": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "扫描系统垃圾文件", + "tags": [ + "Device" + ], + "summary": "Scan system", + "responses": { + "200": { + "description": "OK" + } + }, + "x-panel-log": { + "BeforeFunctions": [], + "bodyKeys": [], + "formatEN": "scan System Junk Files", + "formatZH": "扫描系统垃圾文件", + "paramKeys": [] + } + } + }, + "/websites": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "创建网站", + "consumes": [ + "application/json" + ], + "tags": [ + "Website" + ], + "summary": "Create website", + "parameters": [ + { + "description": "request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/request.WebsiteCreate" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + }, + "x-panel-log": { + "BeforeFunctions": [], + "bodyKeys": [ + "primaryDomain" + ], + "formatEN": "Create website [primaryDomain]", + "formatZH": "创建网站 [primaryDomain]", + "paramKeys": [] + } + } + }, + "/websites/:id": { + "get": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "通过 id 查询网站", + "consumes": [ + "application/json" + ], + "tags": [ + "Website" + ], + "summary": "Search website by id", + "parameters": [ + { + "type": "integer", + "description": "request", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/response.WebsiteDTO" + } + } + } + } + }, + "/websites/:id/config/:type": { + "get": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "通过 id 查询网站 nginx", + "consumes": [ + "application/json" + ], + "tags": [ + "Website Nginx" + ], + "summary": "Search website nginx by id", + "parameters": [ + { + "type": "integer", + "description": "request", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/response.FileInfo" + } + } + } + } + }, + "/websites/:id/https": { + "get": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "获取 https 配置", + "consumes": [ + "application/json" + ], + "tags": [ + "Website HTTPS" + ], + "summary": "Load https conf", + "parameters": [ + { + "type": "integer", + "description": "request", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/response.WebsiteHTTPS" + } + } + } + }, + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "更新 https 配置", + "consumes": [ + "application/json" + ], + "tags": [ + "Website HTTPS" + ], + "summary": "Update https conf", + "parameters": [ + { + "description": "request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/request.WebsiteHTTPSOp" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/response.WebsiteHTTPS" + } + } + }, + "x-panel-log": { + "BeforeFunctions": [ + { + "db": "websites", + "input_column": "id", + "input_value": "websiteId", + "isList": false, + "output_column": "primary_domain", + "output_value": "domain" + } + ], + "bodyKeys": [ + "websiteId" + ], + "formatEN": "Update website https [domain] conf", + "formatZH": "更新网站 [domain] https 配置", + "paramKeys": [] + } + } + }, + "/websites/acme": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "创建网站 acme", + "consumes": [ + "application/json" + ], + "tags": [ + "Website Acme" + ], + "summary": "Create website acme account", + "parameters": [ + { + "description": "request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/request.WebsiteAcmeAccountCreate" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/response.WebsiteAcmeAccountDTO" + } + } + }, + "x-panel-log": { + "BeforeFunctions": [], + "bodyKeys": [ + "email" + ], + "formatEN": "Create website acme [email]", + "formatZH": "创建网站 acme [email]", + "paramKeys": [] + } + } + }, + "/websites/acme/del": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "删除网站 acme", + "consumes": [ + "application/json" + ], + "tags": [ + "Website Acme" + ], + "summary": "Delete website acme account", + "parameters": [ + { + "description": "request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/request.WebsiteResourceReq" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + }, + "x-panel-log": { + "BeforeFunctions": [ + { + "db": "website_acme_accounts", + "input_column": "id", + "input_value": "id", + "isList": false, + "output_column": "email", + "output_value": "email" + } + ], + "bodyKeys": [ + "id" + ], + "formatEN": "Delete website acme [email]", + "formatZH": "删除网站 acme [email]", + "paramKeys": [] + } + } + }, + "/websites/acme/search": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "获取网站 acme 列表分页", + "consumes": [ + "application/json" + ], + "tags": [ + "Website Acme" + ], + "summary": "Page website acme accounts", + "parameters": [ + { + "description": "request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.PageInfo" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/dto.PageResult" + } + } + } + } + }, + "/websites/auths": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "获取密码访问配置", + "consumes": [ + "application/json" + ], + "tags": [ + "Website" + ], + "summary": "Get AuthBasic conf", + "parameters": [ + { + "description": "request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/request.NginxAuthReq" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + } + } + }, + "/websites/auths/update": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "更新密码访问配置", + "consumes": [ + "application/json" + ], + "tags": [ + "Website" + ], + "summary": "Get AuthBasic conf", + "parameters": [ + { + "description": "request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/request.NginxAuthUpdate" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + } + } + }, + "/websites/ca": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "创建网站 ca", + "consumes": [ + "application/json" + ], + "tags": [ + "Website CA" + ], + "summary": "Create website ca", + "parameters": [ + { + "description": "request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/request.WebsiteCACreate" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/request.WebsiteCACreate" + } + } + }, + "x-panel-log": { + "BeforeFunctions": [], + "bodyKeys": [ + "name" + ], + "formatEN": "Create website ca [name]", + "formatZH": "创建网站 ca [name]", + "paramKeys": [] + } + } + }, + "/websites/ca/del": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "删除网站 ca", + "consumes": [ + "application/json" + ], + "tags": [ + "Website CA" + ], + "summary": "Delete website ca", + "parameters": [ + { + "description": "request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/request.WebsiteCommonReq" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + }, + "x-panel-log": { + "BeforeFunctions": [ + { + "db": "website_cas", + "input_column": "id", + "input_value": "id", + "isList": false, + "output_column": "name", + "output_value": "name" + } + ], + "bodyKeys": [ + "id" + ], + "formatEN": "Delete website ca [name]", + "formatZH": "删除网站 ca [name]", + "paramKeys": [] + } + } + }, + "/websites/ca/download": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "下载 CA 证书文件", + "consumes": [ + "application/json" + ], + "tags": [ + "Website CA" + ], + "summary": "Download CA file", + "parameters": [ + { + "description": "request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/request.WebsiteResourceReq" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + }, + "x-panel-log": { + "BeforeFunctions": [ + { + "db": "website_cas", + "input_column": "id", + "input_value": "id", + "isList": false, + "output_column": "name", + "output_value": "name" + } + ], + "bodyKeys": [ + "id" + ], + "formatEN": "download ca file [name]", + "formatZH": "下载 CA 证书文件 [name]", + "paramKeys": [] + } + } + }, + "/websites/ca/obtain": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "自签 SSL 证书", + "consumes": [ + "application/json" + ], + "tags": [ + "Website CA" + ], + "summary": "Obtain SSL", + "parameters": [ + { + "description": "request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/request.WebsiteCAObtain" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + }, + "x-panel-log": { + "BeforeFunctions": [ + { + "db": "website_cas", + "input_column": "id", + "input_value": "id", + "isList": false, + "output_column": "name", + "output_value": "name" + } + ], + "bodyKeys": [ + "id" + ], + "formatEN": "Obtain SSL [name]", + "formatZH": "自签 SSL 证书 [name]", + "paramKeys": [] + } + } + }, + "/websites/ca/renew": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "续签 SSL 证书", + "consumes": [ + "application/json" + ], + "tags": [ + "Website CA" + ], + "summary": "Obtain SSL", + "parameters": [ + { + "description": "request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/request.WebsiteCAObtain" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + }, + "x-panel-log": { + "BeforeFunctions": [ + { + "db": "website_cas", + "input_column": "id", + "input_value": "id", + "isList": false, + "output_column": "name", + "output_value": "name" + } + ], + "bodyKeys": [ + "id" + ], + "formatEN": "Obtain SSL [name]", + "formatZH": "自签 SSL 证书 [name]", + "paramKeys": [] + } + } + }, + "/websites/ca/search": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "获取网站 ca 列表分页", + "consumes": [ + "application/json" + ], + "tags": [ + "Website CA" + ], + "summary": "Page website ca", + "parameters": [ + { + "description": "request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/request.WebsiteCASearch" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/dto.PageResult" + } + } + } + } + }, + "/websites/ca/{id}": { + "get": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "获取网站 ca", + "consumes": [ + "application/json" + ], + "tags": [ + "Website CA" + ], + "summary": "Get website ca", + "parameters": [ + { + "type": "integer", + "description": "id", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/response.WebsiteCADTO" + } + } + } + } + }, + "/websites/check": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "网站创建前检查", + "consumes": [ + "application/json" + ], + "tags": [ + "Website" + ], + "summary": "Check before create website", + "parameters": [ + { + "description": "request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/request.WebsiteInstallCheckReq" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/response.WebsitePreInstallCheck" + } + } + } + } + } + }, + "/websites/config": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "获取 nginx 配置", + "consumes": [ + "application/json" + ], + "tags": [ + "Website Nginx" + ], + "summary": "Load nginx conf", + "parameters": [ + { + "description": "request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/request.NginxScopeReq" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/response.WebsiteNginxConfig" + } + } + } + } + }, + "/websites/config/update": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "更新 nginx 配置", + "consumes": [ + "application/json" + ], + "tags": [ + "Website Nginx" + ], + "summary": "Update nginx conf", + "parameters": [ + { + "description": "request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/request.NginxConfigUpdate" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + }, + "x-panel-log": { + "BeforeFunctions": [ + { + "db": "websites", + "input_column": "id", + "input_value": "websiteId", + "isList": false, + "output_column": "primary_domain", + "output_value": "domain" + } + ], + "bodyKeys": [ + "websiteId" + ], + "formatEN": "Nginx conf update [domain]", + "formatZH": "nginx 配置修改 [domain]", + "paramKeys": [] + } + } + }, + "/websites/default/html/:type": { + "get": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "获取默认 html", + "consumes": [ + "application/json" + ], + "tags": [ + "Website" + ], + "summary": "Get default html", + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/response.FileInfo" + } + } + } + } + }, + "/websites/default/html/update": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "更新默认 html", + "consumes": [ + "application/json" + ], + "tags": [ + "Website" + ], + "summary": "Update default html", + "parameters": [ + { + "description": "request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/request.WebsiteHtmlUpdate" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + }, + "x-panel-log": { + "BeforeFunctions": [], + "bodyKeys": [ + "type" + ], + "formatEN": "Update default html", + "formatZH": "更新默认 html", + "paramKeys": [] + } + } + }, + "/websites/default/server": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "操作网站日志", + "consumes": [ + "application/json" + ], + "tags": [ + "Website" + ], + "summary": "Change default server", + "parameters": [ + { + "description": "request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/request.WebsiteDefaultUpdate" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + }, + "x-panel-log": { + "BeforeFunctions": [ + { + "db": "websites", + "input_column": "id", + "input_value": "id", + "isList": false, + "output_column": "primary_domain", + "output_value": "domain" + } + ], + "bodyKeys": [ + "id", + "operate" + ], + "formatEN": "Change default server =\u003e [domain]", + "formatZH": "修改默认 server =\u003e [domain]", + "paramKeys": [] + } + } + }, + "/websites/del": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "删除网站", + "consumes": [ + "application/json" + ], + "tags": [ + "Website" + ], + "summary": "Delete website", + "parameters": [ + { + "description": "request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/request.WebsiteDelete" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + }, + "x-panel-log": { + "BeforeFunctions": [ + { + "db": "websites", + "input_column": "id", + "input_value": "id", + "isList": false, + "output_column": "primary_domain", + "output_value": "domain" + } + ], + "bodyKeys": [ + "id" + ], + "formatEN": "Delete website [domain]", + "formatZH": "删除网站 [domain]", + "paramKeys": [] + } + } + }, + "/websites/dir": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "获取网站目录配置", + "consumes": [ + "application/json" + ], + "tags": [ + "Website" + ], + "summary": "Get website dir", + "parameters": [ + { + "description": "request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/request.WebsiteCommonReq" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + } + } + }, + "/websites/dir/permission": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "更新网站目录权限", + "consumes": [ + "application/json" + ], + "tags": [ + "Website" + ], + "summary": "Update Site Dir permission", + "parameters": [ + { + "description": "request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/request.WebsiteUpdateDirPermission" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + }, + "x-panel-log": { + "BeforeFunctions": [ + { + "db": "websites", + "input_column": "id", + "input_value": "id", + "isList": false, + "output_column": "primary_domain", + "output_value": "domain" + } + ], + "bodyKeys": [ + "id" + ], + "formatEN": "Update domain [domain] dir permission", + "formatZH": "更新网站 [domain] 目录权限", + "paramKeys": [] + } + } + }, + "/websites/dir/update": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "更新网站目录", + "consumes": [ + "application/json" + ], + "tags": [ + "Website" + ], + "summary": "Update Site Dir", + "parameters": [ + { + "description": "request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/request.WebsiteUpdateDir" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + }, + "x-panel-log": { + "BeforeFunctions": [ + { + "db": "websites", + "input_column": "id", + "input_value": "id", + "isList": false, + "output_column": "primary_domain", + "output_value": "domain" + } + ], + "bodyKeys": [ + "id" + ], + "formatEN": "Update domain [domain] dir", + "formatZH": "更新网站 [domain] 目录", + "paramKeys": [] + } + } + }, + "/websites/dns": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "创建网站 dns", + "consumes": [ + "application/json" + ], + "tags": [ + "Website DNS" + ], + "summary": "Create website dns account", + "parameters": [ + { + "description": "request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/request.WebsiteDnsAccountCreate" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + }, + "x-panel-log": { + "BeforeFunctions": [], + "bodyKeys": [ + "name" + ], + "formatEN": "Create website dns [name]", + "formatZH": "创建网站 dns [name]", + "paramKeys": [] + } + } + }, + "/websites/dns/del": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "删除网站 dns", + "consumes": [ + "application/json" + ], + "tags": [ + "Website DNS" + ], + "summary": "Delete website dns account", + "parameters": [ + { + "description": "request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/request.WebsiteResourceReq" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + }, + "x-panel-log": { + "BeforeFunctions": [ + { + "db": "website_dns_accounts", + "input_column": "id", + "input_value": "id", + "isList": false, + "output_column": "name", + "output_value": "name" + } + ], + "bodyKeys": [ + "id" + ], + "formatEN": "Delete website dns [name]", + "formatZH": "删除网站 dns [name]", + "paramKeys": [] + } + } + }, + "/websites/dns/search": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "获取网站 dns 列表分页", + "consumes": [ + "application/json" + ], + "tags": [ + "Website DNS" + ], + "summary": "Page website dns accounts", + "parameters": [ + { + "description": "request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.PageInfo" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/dto.PageResult" + } + } + } + } + }, + "/websites/dns/update": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "更新网站 dns", + "consumes": [ + "application/json" + ], + "tags": [ + "Website DNS" + ], + "summary": "Update website dns account", + "parameters": [ + { + "description": "request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/request.WebsiteDnsAccountUpdate" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + }, + "x-panel-log": { + "BeforeFunctions": [], + "bodyKeys": [ + "name" + ], + "formatEN": "Update website dns [name]", + "formatZH": "更新网站 dns [name]", + "paramKeys": [] + } + } + }, + "/websites/domains": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "创建网站域名", + "consumes": [ + "application/json" + ], + "tags": [ + "Website Domain" + ], + "summary": "Create website domain", + "parameters": [ + { + "description": "request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/request.WebsiteDomainCreate" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/model.WebsiteDomain" + } + } + }, + "x-panel-log": { + "BeforeFunctions": [], + "bodyKeys": [ + "domain" + ], + "formatEN": "Create domain [domain]", + "formatZH": "创建域名 [domain]", + "paramKeys": [] + } + } + }, + "/websites/domains/:websiteId": { + "get": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "通过网站 id 查询域名", + "consumes": [ + "application/json" + ], + "tags": [ + "Website Domain" + ], + "summary": "Search website domains by websiteId", + "parameters": [ + { + "type": "integer", + "description": "request", + "name": "websiteId", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/model.WebsiteDomain" + } + } + } + } + } + }, + "/websites/domains/del": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "删除网站域名", + "consumes": [ + "application/json" + ], + "tags": [ + "Website Domain" + ], + "summary": "Delete website domain", + "parameters": [ + { + "description": "request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/request.WebsiteDomainDelete" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + }, + "x-panel-log": { + "BeforeFunctions": [ + { + "db": "website_domains", + "input_column": "id", + "input_value": "id", + "isList": false, + "output_column": "domain", + "output_value": "domain" + } + ], + "bodyKeys": [ + "id" + ], + "formatEN": "Delete domain [domain]", + "formatZH": "删除域名 [domain]", + "paramKeys": [] + } + } + }, + "/websites/leech": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "获取防盗链配置", + "consumes": [ + "application/json" + ], + "tags": [ + "Website" + ], + "summary": "Get AntiLeech conf", + "parameters": [ + { + "description": "request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/request.NginxCommonReq" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + } + } + }, + "/websites/leech/update": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "更新防盗链配置", + "consumes": [ + "application/json" + ], + "tags": [ + "Website" + ], + "summary": "Update AntiLeech", + "parameters": [ + { + "description": "request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/request.NginxAntiLeechUpdate" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + } + } + }, + "/websites/list": { + "get": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "获取网站列表", + "tags": [ + "Website" + ], + "summary": "List websites", + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/response.WebsiteDTO" + } + } + } + } + } + }, + "/websites/log": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "操作网站日志", + "consumes": [ + "application/json" + ], + "tags": [ + "Website" + ], + "summary": "Operate website log", + "parameters": [ + { + "description": "request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/request.WebsiteLogReq" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/response.WebsiteLog" + } + } + }, + "x-panel-log": { + "BeforeFunctions": [ + { + "db": "websites", + "input_column": "id", + "input_value": "id", + "isList": false, + "output_column": "primary_domain", + "output_value": "domain" + } + ], + "bodyKeys": [ + "id", + "operate" + ], + "formatEN": "[domain][operate] logs", + "formatZH": "[domain][operate] 日志", + "paramKeys": [] + } + } + }, + "/websites/nginx/update": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "更新 网站 nginx 配置", + "consumes": [ + "application/json" + ], + "tags": [ + "Website Nginx" + ], + "summary": "Update website nginx conf", + "parameters": [ + { + "description": "request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/request.WebsiteNginxUpdate" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + }, + "x-panel-log": { + "BeforeFunctions": [ + { + "db": "websites", + "input_column": "id", + "input_value": "id", + "isList": false, + "output_column": "primary_domain", + "output_value": "domain" + } + ], + "bodyKeys": [ + "id" + ], + "formatEN": "[domain] Nginx conf update", + "formatZH": "[domain] Nginx 配置修改", + "paramKeys": [] + } + } + }, + "/websites/operate": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "操作网站", + "consumes": [ + "application/json" + ], + "tags": [ + "Website" + ], + "summary": "Operate website", + "parameters": [ + { + "description": "request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/request.WebsiteOp" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + }, + "x-panel-log": { + "BeforeFunctions": [ + { + "db": "websites", + "input_column": "id", + "input_value": "id", + "isList": false, + "output_column": "primary_domain", + "output_value": "domain" + } + ], + "bodyKeys": [ + "id", + "operate" + ], + "formatEN": "[operate] website [domain]", + "formatZH": "[operate] 网站 [domain]", + "paramKeys": [] + } + } + }, + "/websites/options": { + "get": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "获取网站列表", + "tags": [ + "Website" + ], + "summary": "List website names", + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "array", + "items": { + "type": "string" + } + } + } + } + } + }, + "/websites/php/config": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "更新 网站 PHP 配置", + "consumes": [ + "application/json" + ], + "tags": [ + "Website PHP" + ], + "summary": "Update website php conf", + "parameters": [ + { + "description": "request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/request.WebsitePHPConfigUpdate" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + }, + "x-panel-log": { + "BeforeFunctions": [ + { + "db": "websites", + "input_column": "id", + "input_value": "id", + "isList": false, + "output_column": "primary_domain", + "output_value": "domain" + } + ], + "bodyKeys": [ + "id" + ], + "formatEN": "[domain] PHP conf update", + "formatZH": "[domain] PHP 配置修改", + "paramKeys": [] + } + } + }, + "/websites/php/config/:id": { + "get": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "获取网站 php 配置", + "consumes": [ + "application/json" + ], + "tags": [ + "Website" + ], + "summary": "Load website php conf", + "parameters": [ + { + "type": "integer", + "description": "request", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/response.PHPConfig" + } + } + } + } + }, + "/websites/php/update": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "更新 php 配置文件", + "consumes": [ + "application/json" + ], + "tags": [ + "Website PHP" + ], + "summary": "Update php conf", + "parameters": [ + { + "description": "request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/request.WebsitePHPFileUpdate" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + }, + "x-panel-log": { + "BeforeFunctions": [ + { + "db": "websites", + "input_column": "id", + "input_value": "websiteId", + "isList": false, + "output_column": "primary_domain", + "output_value": "domain" + } + ], + "bodyKeys": [ + "websiteId" + ], + "formatEN": "Nginx conf update [domain]", + "formatZH": "php 配置修改 [domain]", + "paramKeys": [] + } + } + }, + "/websites/php/version": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "变更 php 版本", + "consumes": [ + "application/json" + ], + "tags": [ + "Website PHP" + ], + "summary": "Update php version", + "parameters": [ + { + "description": "request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/request.WebsitePHPVersionReq" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + }, + "x-panel-log": { + "BeforeFunctions": [ + { + "db": "websites", + "input_column": "id", + "input_value": "websiteId", + "isList": false, + "output_column": "primary_domain", + "output_value": "domain" + } + ], + "bodyKeys": [ + "websiteId" + ], + "formatEN": "php version update [domain]", + "formatZH": "php 版本变更 [domain]", + "paramKeys": [] + } + } + }, + "/websites/proxies": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "获取反向代理配置", + "consumes": [ + "application/json" + ], + "tags": [ + "Website" + ], + "summary": "Get proxy conf", + "parameters": [ + { + "description": "request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/request.WebsiteProxyReq" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + } + } + }, + "/websites/proxies/update": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "修改反向代理配置", + "consumes": [ + "application/json" + ], + "tags": [ + "Website" + ], + "summary": "Update proxy conf", + "parameters": [ + { + "description": "request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/request.WebsiteProxyConfig" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + }, + "x-panel-log": { + "BeforeFunctions": [ + { + "db": "websites", + "input_column": "id", + "input_value": "id", + "isList": false, + "output_column": "primary_domain", + "output_value": "domain" + } + ], + "bodyKeys": [ + "id" + ], + "formatEN": "Update domain [domain] proxy config", + "formatZH": "修改网站 [domain] 反向代理配置 ", + "paramKeys": [] + } + } + }, + "/websites/proxy/file": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "更新反向代理文件", + "consumes": [ + "application/json" + ], + "tags": [ + "Website" + ], + "summary": "Update proxy file", + "parameters": [ + { + "description": "request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/request.NginxProxyUpdate" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + }, + "x-panel-log": { + "BeforeFunctions": [ + { + "db": "websites", + "input_column": "id", + "input_value": "websiteID", + "isList": false, + "output_column": "primary_domain", + "output_value": "domain" + } + ], + "bodyKeys": [ + "websiteID" + ], + "formatEN": "Nginx conf proxy file update [domain]", + "formatZH": "更新反向代理文件 [domain]", + "paramKeys": [] + } + } + }, + "/websites/redirect": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "获取重定向配置", + "consumes": [ + "application/json" + ], + "tags": [ + "Website" + ], + "summary": "Get redirect conf", + "parameters": [ + { + "description": "request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/request.WebsiteProxyReq" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + } + } + }, + "/websites/redirect/file": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "更新重定向文件", + "consumes": [ + "application/json" + ], + "tags": [ + "Website" + ], + "summary": "Update redirect file", + "parameters": [ + { + "description": "request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/request.NginxRedirectUpdate" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + }, + "x-panel-log": { + "BeforeFunctions": [ + { + "db": "websites", + "input_column": "id", + "input_value": "websiteID", + "isList": false, + "output_column": "primary_domain", + "output_value": "domain" + } + ], + "bodyKeys": [ + "websiteID" + ], + "formatEN": "Nginx conf redirect file update [domain]", + "formatZH": "更新重定向文件 [domain]", + "paramKeys": [] + } + } + }, + "/websites/redirect/update": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "修改重定向配置", + "consumes": [ + "application/json" + ], + "tags": [ + "Website" + ], + "summary": "Update redirect conf", + "parameters": [ + { + "description": "request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/request.NginxRedirectReq" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + }, + "x-panel-log": { + "BeforeFunctions": [ + { + "db": "websites", + "input_column": "id", + "input_value": "websiteID", + "isList": false, + "output_column": "primary_domain", + "output_value": "domain" + } + ], + "bodyKeys": [ + "websiteID" + ], + "formatEN": "Update domain [domain] redirect config", + "formatZH": "修改网站 [domain] 重定向理配置 ", + "paramKeys": [] + } + } + }, + "/websites/rewrite": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "获取伪静态配置", + "consumes": [ + "application/json" + ], + "tags": [ + "Website" + ], + "summary": "Get rewrite conf", + "parameters": [ + { + "description": "request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/request.NginxRewriteReq" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + } + } + }, + "/websites/rewrite/update": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "更新伪静态配置", + "consumes": [ + "application/json" + ], + "tags": [ + "Website" + ], + "summary": "Update rewrite conf", + "parameters": [ + { + "description": "request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/request.NginxRewriteUpdate" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + }, + "x-panel-log": { + "BeforeFunctions": [ + { + "db": "websites", + "input_column": "id", + "input_value": "websiteID", + "isList": false, + "output_column": "primary_domain", + "output_value": "domain" + } + ], + "bodyKeys": [ + "websiteID" + ], + "formatEN": "Nginx conf rewrite update [domain]", + "formatZH": "伪静态配置修改 [domain]", + "paramKeys": [] + } + } + }, + "/websites/search": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "获取网站列表分页", + "consumes": [ + "application/json" + ], + "tags": [ + "Website" + ], + "summary": "Page websites", + "parameters": [ + { + "description": "request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/request.WebsiteSearch" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/dto.PageResult" + } + } + } + } + }, + "/websites/ssl": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "创建网站 ssl", + "consumes": [ + "application/json" + ], + "tags": [ + "Website SSL" + ], + "summary": "Create website ssl", + "parameters": [ + { + "description": "request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/request.WebsiteSSLCreate" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/request.WebsiteSSLCreate" + } + } + }, + "x-panel-log": { + "BeforeFunctions": [], + "bodyKeys": [ + "primaryDomain" + ], + "formatEN": "Create website ssl [primaryDomain]", + "formatZH": "创建网站 ssl [primaryDomain]", + "paramKeys": [] + } + } + }, + "/websites/ssl/:id": { + "get": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "通过 id 查询 ssl", + "consumes": [ + "application/json" + ], + "tags": [ + "Website SSL" + ], + "summary": "Search website ssl by id", + "parameters": [ + { + "type": "integer", + "description": "request", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK" + } + } + } + }, + "/websites/ssl/del": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "删除网站 ssl", + "consumes": [ + "application/json" + ], + "tags": [ + "Website SSL" + ], + "summary": "Delete website ssl", + "parameters": [ + { + "description": "request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/request.WebsiteBatchDelReq" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + }, + "x-panel-log": { + "BeforeFunctions": [ + { + "db": "website_ssls", + "input_column": "id", + "input_value": "ids", + "isList": true, + "output_column": "primary_domain", + "output_value": "domain" + } + ], + "bodyKeys": [ + "ids" + ], + "formatEN": "Delete ssl [domain]", + "formatZH": "删除 ssl [domain]", + "paramKeys": [] + } + } + }, + "/websites/ssl/download": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "下载证书文件", + "consumes": [ + "application/json" + ], + "tags": [ + "Website SSL" + ], + "summary": "Download SSL file", + "parameters": [ + { + "description": "request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/request.WebsiteResourceReq" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + }, + "x-panel-log": { + "BeforeFunctions": [ + { + "db": "website_ssls", + "input_column": "id", + "input_value": "id", + "isList": false, + "output_column": "primary_domain", + "output_value": "domain" + } + ], + "bodyKeys": [ + "id" + ], + "formatEN": "download ssl file [domain]", + "formatZH": "下载证书文件 [domain]", + "paramKeys": [] + } + } + }, + "/websites/ssl/obtain": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "申请证书", + "consumes": [ + "application/json" + ], + "tags": [ + "Website SSL" + ], + "summary": "Apply ssl", + "parameters": [ + { + "description": "request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/request.WebsiteSSLApply" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + }, + "x-panel-log": { + "BeforeFunctions": [ + { + "db": "website_ssls", + "input_column": "id", + "input_value": "ID", + "isList": false, + "output_column": "primary_domain", + "output_value": "domain" + } + ], + "bodyKeys": [ + "ID" + ], + "formatEN": "apply ssl [domain]", + "formatZH": "申请证书 [domain]", + "paramKeys": [] + } + } + }, + "/websites/ssl/resolve": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "解析网站 ssl", + "consumes": [ + "application/json" + ], + "tags": [ + "Website SSL" + ], + "summary": "Resolve website ssl", + "parameters": [ + { + "description": "request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/request.WebsiteDNSReq" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/response.WebsiteDNSRes" + } + } + } + } + } + }, + "/websites/ssl/search": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "获取网站 ssl 列表分页", + "consumes": [ + "application/json" + ], + "tags": [ + "Website SSL" + ], + "summary": "Page website ssl", + "parameters": [ + { + "description": "request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/request.WebsiteSSLSearch" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + } + } + }, + "/websites/ssl/update": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "更新 ssl", + "consumes": [ + "application/json" + ], + "tags": [ + "Website SSL" + ], + "summary": "Update ssl", + "parameters": [ + { + "description": "request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/request.WebsiteSSLUpdate" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + }, + "x-panel-log": { + "BeforeFunctions": [ + { + "db": "website_ssls", + "input_column": "id", + "input_value": "id", + "isList": false, + "output_column": "primary_domain", + "output_value": "domain" + } + ], + "bodyKeys": [ + "id" + ], + "formatEN": "Update ssl config [domain]", + "formatZH": "更新证书设置 [domain]", + "paramKeys": [] + } + } + }, + "/websites/ssl/upload": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "上传 ssl", + "consumes": [ + "application/json" + ], + "tags": [ + "Website SSL" + ], + "summary": "Upload ssl", + "parameters": [ + { + "description": "request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/request.WebsiteSSLUpload" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + }, + "x-panel-log": { + "BeforeFunctions": [], + "bodyKeys": [ + "type" + ], + "formatEN": "Upload ssl [type]", + "formatZH": "上传 ssl [type]", + "paramKeys": [] + } + } + }, + "/websites/ssl/website/:websiteId": { + "get": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "通过网站 id 查询 ssl", + "consumes": [ + "application/json" + ], + "tags": [ + "Website SSL" + ], + "summary": "Search website ssl by website id", + "parameters": [ + { + "type": "integer", + "description": "request", + "name": "websiteId", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK" + } + } + } + }, + "/websites/update": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "更新网站", + "consumes": [ + "application/json" + ], + "tags": [ + "Website" + ], + "summary": "Update website", + "parameters": [ + { + "description": "request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/request.WebsiteUpdate" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + }, + "x-panel-log": { + "BeforeFunctions": [], + "bodyKeys": [ + "primaryDomain" + ], + "formatEN": "Update website [primaryDomain]", + "formatZH": "更新网站 [primaryDomain]", + "paramKeys": [] + } + } + } + }, + "definitions": { + "dto.AddrRuleOperate": { + "type": "object", + "required": [ + "address", + "operation", + "strategy" + ], + "properties": { + "address": { + "type": "string" + }, + "description": { + "type": "string" + }, + "operation": { + "type": "string", + "enum": [ + "add", + "remove" + ] + }, + "strategy": { + "type": "string", + "enum": [ + "accept", + "drop" + ] + } + } + }, + "dto.AddrRuleUpdate": { + "type": "object", + "properties": { + "newRule": { + "$ref": "#/definitions/dto.AddrRuleOperate" + }, + "oldRule": { + "$ref": "#/definitions/dto.AddrRuleOperate" + } + } + }, + "dto.AppInstallInfo": { + "type": "object", + "properties": { + "id": { + "type": "integer" + }, + "key": { + "type": "string" + }, + "name": { + "type": "string" + } + } + }, + "dto.AppResource": { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "type": { + "type": "string" + } + } + }, + "dto.AppVersion": { + "type": "object", + "properties": { + "detailId": { + "type": "integer" + }, + "dockerCompose": { + "type": "string" + }, + "version": { + "type": "string" + } + } + }, + "dto.BackupInfo": { + "type": "object", + "properties": { + "backupPath": { + "type": "string" + }, + "bucket": { + "type": "string" + }, + "createdAt": { + "type": "string" + }, + "id": { + "type": "integer" + }, + "type": { + "type": "string" + }, + "vars": { + "type": "string" + } + } + }, + "dto.BackupOperate": { + "type": "object", + "required": [ + "type", + "vars" + ], + "properties": { + "accessKey": { + "type": "string" + }, + "backupPath": { + "type": "string" + }, + "bucket": { + "type": "string" + }, + "credential": { + "type": "string" + }, + "id": { + "type": "integer" + }, + "type": { + "type": "string" + }, + "vars": { + "type": "string" + } + } + }, + "dto.BackupSearchFile": { + "type": "object", + "required": [ + "type" + ], + "properties": { + "type": { + "type": "string" + } + } + }, + "dto.BatchDelete": { + "type": "object", + "required": [ + "names" + ], + "properties": { + "force": { + "type": "boolean" + }, + "names": { + "type": "array", + "items": { + "type": "string" + } + } + } + }, + "dto.BatchDeleteReq": { + "type": "object", + "required": [ + "ids" + ], + "properties": { + "ids": { + "type": "array", + "items": { + "type": "integer" + } + } + } + }, + "dto.BatchRuleOperate": { + "type": "object", + "required": [ + "type" + ], + "properties": { + "rules": { + "type": "array", + "items": { + "$ref": "#/definitions/dto.PortRuleOperate" + } + }, + "type": { + "type": "string" + } + } + }, + "dto.BindInfo": { + "type": "object", + "required": [ + "bindAddress", + "ipv6" + ], + "properties": { + "bindAddress": { + "type": "string" + }, + "ipv6": { + "type": "string", + "enum": [ + "enable", + "disable" + ] + } + } + }, + "dto.BindUser": { + "type": "object", + "required": [ + "database", + "db", + "password", + "permission", + "username" + ], + "properties": { + "database": { + "type": "string" + }, + "db": { + "type": "string" + }, + "password": { + "type": "string" + }, + "permission": { + "type": "string" + }, + "username": { + "type": "string" + } + } + }, + "dto.CaptchaResponse": { + "type": "object", + "properties": { + "captchaID": { + "type": "string" + }, + "imagePath": { + "type": "string" + } + } + }, + "dto.ChangeDBInfo": { + "type": "object", + "required": [ + "database", + "from", + "type", + "value" + ], + "properties": { + "database": { + "type": "string" + }, + "from": { + "type": "string", + "enum": [ + "local", + "remote" + ] + }, + "id": { + "type": "integer" + }, + "type": { + "type": "string", + "enum": [ + "mysql", + "mariadb", + "postgresql" + ] + }, + "value": { + "type": "string" + } + } + }, + "dto.ChangeHostGroup": { + "type": "object", + "required": [ + "groupID", + "id" + ], + "properties": { + "groupID": { + "type": "integer" + }, + "id": { + "type": "integer" + } + } + }, + "dto.ChangePasswd": { + "type": "object", + "properties": { + "passwd": { + "type": "string" + }, + "user": { + "type": "string" + } + } + }, + "dto.ChangeRedisPass": { + "type": "object", + "required": [ + "database" + ], + "properties": { + "database": { + "type": "string" + }, + "value": { + "type": "string" + } + } + }, + "dto.ClamBaseInfo": { + "type": "object", + "properties": { + "freshIsActive": { + "type": "boolean" + }, + "freshIsExist": { + "type": "boolean" + }, + "freshVersion": { + "type": "string" + }, + "isActive": { + "type": "boolean" + }, + "isExist": { + "type": "boolean" + }, + "version": { + "type": "string" + } + } + }, + "dto.ClamCreate": { + "type": "object", + "properties": { + "description": { + "type": "string" + }, + "infectedDir": { + "type": "string" + }, + "infectedStrategy": { + "type": "string" + }, + "name": { + "type": "string" + }, + "path": { + "type": "string" + }, + "spec": { + "type": "string" + }, + "status": { + "type": "string" + } + } + }, + "dto.ClamDelete": { + "type": "object", + "required": [ + "ids" + ], + "properties": { + "ids": { + "type": "array", + "items": { + "type": "integer" + } + }, + "removeInfected": { + "type": "boolean" + }, + "removeRecord": { + "type": "boolean" + } + } + }, + "dto.ClamFileReq": { + "type": "object", + "required": [ + "name" + ], + "properties": { + "name": { + "type": "string" + }, + "tail": { + "type": "string" + } + } + }, + "dto.ClamLogReq": { + "type": "object", + "properties": { + "clamName": { + "type": "string" + }, + "recordName": { + "type": "string" + }, + "tail": { + "type": "string" + } + } + }, + "dto.ClamLogSearch": { + "type": "object", + "required": [ + "page", + "pageSize" + ], + "properties": { + "clamID": { + "type": "integer" + }, + "endTime": { + "type": "string" + }, + "page": { + "type": "integer" + }, + "pageSize": { + "type": "integer" + }, + "startTime": { + "type": "string" + } + } + }, + "dto.ClamUpdate": { + "type": "object", + "properties": { + "description": { + "type": "string" + }, + "id": { + "type": "integer" + }, + "infectedDir": { + "type": "string" + }, + "infectedStrategy": { + "type": "string" + }, + "name": { + "type": "string" + }, + "path": { + "type": "string" + }, + "spec": { + "type": "string" + } + } + }, + "dto.ClamUpdateStatus": { + "type": "object", + "properties": { + "id": { + "type": "integer" + }, + "status": { + "type": "string" + } + } + }, + "dto.Clean": { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "size": { + "type": "integer" + }, + "treeType": { + "type": "string" + } + } + }, + "dto.CleanLog": { + "type": "object", + "required": [ + "logType" + ], + "properties": { + "logType": { + "type": "string", + "enum": [ + "login", + "operation" + ] + } + } + }, + "dto.CommandInfo": { + "type": "object", + "properties": { + "command": { + "type": "string" + }, + "groupBelong": { + "type": "string" + }, + "groupID": { + "type": "integer" + }, + "id": { + "type": "integer" + }, + "name": { + "type": "string" + } + } + }, + "dto.CommandOperate": { + "type": "object", + "required": [ + "command", + "name" + ], + "properties": { + "command": { + "type": "string" + }, + "groupBelong": { + "type": "string" + }, + "groupID": { + "type": "integer" + }, + "id": { + "type": "integer" + }, + "name": { + "type": "string" + } + } + }, + "dto.CommonBackup": { + "type": "object", + "required": [ + "type" + ], + "properties": { + "detailName": { + "type": "string" + }, + "name": { + "type": "string" + }, + "secret": { + "type": "string" + }, + "type": { + "type": "string", + "enum": [ + "app", + "mysql", + "mariadb", + "redis", + "website", + "postgresql" + ] + } + } + }, + "dto.CommonRecover": { + "type": "object", + "required": [ + "source", + "type" + ], + "properties": { + "detailName": { + "type": "string" + }, + "file": { + "type": "string" + }, + "name": { + "type": "string" + }, + "secret": { + "type": "string" + }, + "source": { + "type": "string", + "enum": [ + "OSS", + "S3", + "SFTP", + "MINIO", + "LOCAL", + "COS", + "KODO", + "OneDrive", + "WebDAV" + ] + }, + "type": { + "type": "string", + "enum": [ + "app", + "mysql", + "mariadb", + "redis", + "website", + "postgresql" + ] + } + } + }, + "dto.ComposeCreate": { + "type": "object", + "required": [ + "from" + ], + "properties": { + "file": { + "type": "string" + }, + "from": { + "type": "string", + "enum": [ + "edit", + "path", + "template" + ] + }, + "name": { + "type": "string" + }, + "path": { + "type": "string" + }, + "template": { + "type": "integer" + } + } + }, + "dto.ComposeOperation": { + "type": "object", + "required": [ + "name", + "operation", + "path" + ], + "properties": { + "name": { + "type": "string" + }, + "operation": { + "type": "string", + "enum": [ + "start", + "stop", + "down" + ] + }, + "path": { + "type": "string" + }, + "withFile": { + "type": "boolean" + } + } + }, + "dto.ComposeTemplateCreate": { + "type": "object", + "required": [ + "name" + ], + "properties": { + "content": { + "type": "string" + }, + "description": { + "type": "string" + }, + "name": { + "type": "string" + } + } + }, + "dto.ComposeTemplateInfo": { + "type": "object", + "properties": { + "content": { + "type": "string" + }, + "createdAt": { + "type": "string" + }, + "description": { + "type": "string" + }, + "id": { + "type": "integer" + }, + "name": { + "type": "string" + } + } + }, + "dto.ComposeTemplateUpdate": { + "type": "object", + "properties": { + "content": { + "type": "string" + }, + "description": { + "type": "string" + }, + "id": { + "type": "integer" + } + } + }, + "dto.ComposeUpdate": { + "type": "object", + "required": [ + "content", + "name", + "path" + ], + "properties": { + "content": { + "type": "string" + }, + "name": { + "type": "string" + }, + "path": { + "type": "string" + } + } + }, + "dto.ContainerCommit": { + "type": "object", + "required": [ + "containerID" + ], + "properties": { + "author": { + "type": "string" + }, + "comment": { + "type": "string" + }, + "containerID": { + "type": "string" + }, + "containerName": { + "type": "string" + }, + "newImageName": { + "type": "string" + }, + "pause": { + "type": "boolean" + } + } + }, + "dto.ContainerListStats": { + "type": "object", + "properties": { + "containerID": { + "type": "string" + }, + "cpuPercent": { + "type": "number" + }, + "cpuTotalUsage": { + "type": "integer" + }, + "memoryCache": { + "type": "integer" + }, + "memoryLimit": { + "type": "integer" + }, + "memoryPercent": { + "type": "number" + }, + "memoryUsage": { + "type": "integer" + }, + "percpuUsage": { + "type": "integer" + }, + "systemUsage": { + "type": "integer" + } + } + }, + "dto.ContainerOperate": { + "type": "object", + "required": [ + "image", + "name" + ], + "properties": { + "autoRemove": { + "type": "boolean" + }, + "cmd": { + "type": "array", + "items": { + "type": "string" + } + }, + "containerID": { + "type": "string" + }, + "cpuShares": { + "type": "integer" + }, + "entrypoint": { + "type": "array", + "items": { + "type": "string" + } + }, + "env": { + "type": "array", + "items": { + "type": "string" + } + }, + "exposedPorts": { + "type": "array", + "items": { + "$ref": "#/definitions/dto.PortHelper" + } + }, + "forcePull": { + "type": "boolean" + }, + "image": { + "type": "string" + }, + "ipv4": { + "type": "string" + }, + "ipv6": { + "type": "string" + }, + "labels": { + "type": "array", + "items": { + "type": "string" + } + }, + "memory": { + "type": "number" + }, + "name": { + "type": "string" + }, + "nanoCPUs": { + "type": "number" + }, + "network": { + "type": "string" + }, + "openStdin": { + "type": "boolean" + }, + "privileged": { + "type": "boolean" + }, + "publishAllPorts": { + "type": "boolean" + }, + "restartPolicy": { + "type": "string" + }, + "tty": { + "type": "boolean" + }, + "volumes": { + "type": "array", + "items": { + "$ref": "#/definitions/dto.VolumeHelper" + } + } + } + }, + "dto.ContainerOperation": { + "type": "object", + "required": [ + "names", + "operation" + ], + "properties": { + "names": { + "type": "array", + "items": { + "type": "string" + } + }, + "operation": { + "type": "string", + "enum": [ + "start", + "stop", + "restart", + "kill", + "pause", + "unpause", + "remove" + ] + } + } + }, + "dto.ContainerPrune": { + "type": "object", + "required": [ + "pruneType" + ], + "properties": { + "pruneType": { + "type": "string", + "enum": [ + "container", + "image", + "volume", + "network", + "buildcache" + ] + }, + "withTagAll": { + "type": "boolean" + } + } + }, + "dto.ContainerPruneReport": { + "type": "object", + "properties": { + "deletedNumber": { + "type": "integer" + }, + "spaceReclaimed": { + "type": "integer" + } + } + }, + "dto.ContainerRename": { + "type": "object", + "required": [ + "name", + "newName" + ], + "properties": { + "name": { + "type": "string" + }, + "newName": { + "type": "string" + } + } + }, + "dto.ContainerStats": { + "type": "object", + "properties": { + "cache": { + "type": "number" + }, + "cpuPercent": { + "type": "number" + }, + "ioRead": { + "type": "number" + }, + "ioWrite": { + "type": "number" + }, + "memory": { + "type": "number" + }, + "networkRX": { + "type": "number" + }, + "networkTX": { + "type": "number" + }, + "shotTime": { + "type": "string" + } + } + }, + "dto.ContainerUpgrade": { + "type": "object", + "required": [ + "image", + "name" + ], + "properties": { + "forcePull": { + "type": "boolean" + }, + "image": { + "type": "string" + }, + "name": { + "type": "string" + } + } + }, + "dto.CronjobBatchDelete": { + "type": "object", + "required": [ + "ids" + ], + "properties": { + "cleanData": { + "type": "boolean" + }, + "ids": { + "type": "array", + "items": { + "type": "integer" + } + } + } + }, + "dto.CronjobClean": { + "type": "object", + "required": [ + "cronjobID" + ], + "properties": { + "cleanData": { + "type": "boolean" + }, + "cronjobID": { + "type": "integer" + }, + "isDelete": { + "type": "boolean" + } + } + }, + "dto.CronjobCreate": { + "type": "object", + "required": [ + "name", + "spec", + "type" + ], + "properties": { + "appID": { + "type": "string" + }, + "backupAccounts": { + "type": "string" + }, + "command": { + "type": "string" + }, + "containerName": { + "type": "string" + }, + "dbName": { + "type": "string" + }, + "dbType": { + "type": "string" + }, + "defaultDownload": { + "type": "string" + }, + "exclusionRules": { + "type": "string" + }, + "name": { + "type": "string" + }, + "retainCopies": { + "type": "integer", + "minimum": 1 + }, + "script": { + "type": "string" + }, + "secret": { + "type": "string" + }, + "sourceDir": { + "type": "string" + }, + "spec": { + "type": "string" + }, + "type": { + "type": "string" + }, + "url": { + "type": "string" + }, + "website": { + "type": "string" + } + } + }, + "dto.CronjobDownload": { + "type": "object", + "required": [ + "backupAccountID", + "recordID" + ], + "properties": { + "backupAccountID": { + "type": "integer" + }, + "recordID": { + "type": "integer" + } + } + }, + "dto.CronjobUpdate": { + "type": "object", + "required": [ + "id", + "name", + "spec" + ], + "properties": { + "appID": { + "type": "string" + }, + "backupAccounts": { + "type": "string" + }, + "command": { + "type": "string" + }, + "containerName": { + "type": "string" + }, + "dbName": { + "type": "string" + }, + "dbType": { + "type": "string" + }, + "defaultDownload": { + "type": "string" + }, + "exclusionRules": { + "type": "string" + }, + "id": { + "type": "integer" + }, + "name": { + "type": "string" + }, + "retainCopies": { + "type": "integer", + "minimum": 1 + }, + "script": { + "type": "string" + }, + "secret": { + "type": "string" + }, + "sourceDir": { + "type": "string" + }, + "spec": { + "type": "string" + }, + "url": { + "type": "string" + }, + "website": { + "type": "string" + } + } + }, + "dto.CronjobUpdateStatus": { + "type": "object", + "required": [ + "id", + "status" + ], + "properties": { + "id": { + "type": "integer" + }, + "status": { + "type": "string" + } + } + }, + "dto.DBBaseInfo": { + "type": "object", + "properties": { + "containerName": { + "type": "string" + }, + "name": { + "type": "string" + }, + "port": { + "type": "integer" + } + } + }, + "dto.DBConfUpdateByFile": { + "type": "object", + "required": [ + "database", + "type" + ], + "properties": { + "database": { + "type": "string" + }, + "file": { + "type": "string" + }, + "type": { + "type": "string", + "enum": [ + "mysql", + "mariadb", + "postgresql", + "redis" + ] + } + } + }, + "dto.DaemonJsonConf": { + "type": "object", + "properties": { + "cgroupDriver": { + "type": "string" + }, + "experimental": { + "type": "boolean" + }, + "fixedCidrV6": { + "type": "string" + }, + "insecureRegistries": { + "type": "array", + "items": { + "type": "string" + } + }, + "ip6Tables": { + "type": "boolean" + }, + "iptables": { + "type": "boolean" + }, + "ipv6": { + "type": "boolean" + }, + "isSwarm": { + "type": "boolean" + }, + "liveRestore": { + "type": "boolean" + }, + "logMaxFile": { + "type": "string" + }, + "logMaxSize": { + "type": "string" + }, + "registryMirrors": { + "type": "array", + "items": { + "type": "string" + } + }, + "status": { + "type": "string" + }, + "version": { + "type": "string" + } + } + }, + "dto.DaemonJsonUpdateByFile": { + "type": "object", + "properties": { + "file": { + "type": "string" + } + } + }, + "dto.DashboardBase": { + "type": "object", + "properties": { + "appInstalledNumber": { + "type": "integer" + }, + "cpuCores": { + "type": "integer" + }, + "cpuLogicalCores": { + "type": "integer" + }, + "cpuModelName": { + "type": "string" + }, + "cronjobNumber": { + "type": "integer" + }, + "currentInfo": { + "$ref": "#/definitions/dto.DashboardCurrent" + }, + "databaseNumber": { + "type": "integer" + }, + "hostname": { + "type": "string" + }, + "kernelArch": { + "type": "string" + }, + "kernelVersion": { + "type": "string" + }, + "os": { + "type": "string" + }, + "platform": { + "type": "string" + }, + "platformFamily": { + "type": "string" + }, + "platformVersion": { + "type": "string" + }, + "virtualizationSystem": { + "type": "string" + }, + "websiteNumber": { + "type": "integer" + } + } + }, + "dto.DashboardCurrent": { + "type": "object", + "properties": { + "cpuPercent": { + "type": "array", + "items": { + "type": "number" + } + }, + "cpuTotal": { + "type": "integer" + }, + "cpuUsed": { + "type": "number" + }, + "cpuUsedPercent": { + "type": "number" + }, + "diskData": { + "type": "array", + "items": { + "$ref": "#/definitions/dto.DiskInfo" + } + }, + "gpuData": { + "type": "array", + "items": { + "$ref": "#/definitions/dto.GPUInfo" + } + }, + "ioCount": { + "type": "integer" + }, + "ioReadBytes": { + "type": "integer" + }, + "ioReadTime": { + "type": "integer" + }, + "ioWriteBytes": { + "type": "integer" + }, + "ioWriteTime": { + "type": "integer" + }, + "load1": { + "type": "number" + }, + "load15": { + "type": "number" + }, + "load5": { + "type": "number" + }, + "loadUsagePercent": { + "type": "number" + }, + "memoryAvailable": { + "type": "integer" + }, + "memoryTotal": { + "type": "integer" + }, + "memoryUsed": { + "type": "integer" + }, + "memoryUsedPercent": { + "type": "number" + }, + "netBytesRecv": { + "type": "integer" + }, + "netBytesSent": { + "type": "integer" + }, + "procs": { + "type": "integer" + }, + "shotTime": { + "type": "string" + }, + "swapMemoryAvailable": { + "type": "integer" + }, + "swapMemoryTotal": { + "type": "integer" + }, + "swapMemoryUsed": { + "type": "integer" + }, + "swapMemoryUsedPercent": { + "type": "number" + }, + "timeSinceUptime": { + "type": "string" + }, + "uptime": { + "type": "integer" + } + } + }, + "dto.DatabaseCreate": { + "type": "object", + "required": [ + "from", + "name", + "type", + "username", + "version" + ], + "properties": { + "address": { + "type": "string" + }, + "clientCert": { + "type": "string" + }, + "clientKey": { + "type": "string" + }, + "description": { + "type": "string" + }, + "from": { + "type": "string", + "enum": [ + "local", + "remote" + ] + }, + "name": { + "type": "string", + "maxLength": 256 + }, + "password": { + "type": "string" + }, + "port": { + "type": "integer" + }, + "rootCert": { + "type": "string" + }, + "skipVerify": { + "type": "boolean" + }, + "ssl": { + "type": "boolean" + }, + "type": { + "type": "string" + }, + "username": { + "type": "string" + }, + "version": { + "type": "string" + } + } + }, + "dto.DatabaseDelete": { + "type": "object", + "required": [ + "id" + ], + "properties": { + "deleteBackup": { + "type": "boolean" + }, + "forceDelete": { + "type": "boolean" + }, + "id": { + "type": "integer" + } + } + }, + "dto.DatabaseInfo": { + "type": "object", + "properties": { + "address": { + "type": "string" + }, + "clientCert": { + "type": "string" + }, + "clientKey": { + "type": "string" + }, + "createdAt": { + "type": "string" + }, + "description": { + "type": "string" + }, + "from": { + "type": "string" + }, + "id": { + "type": "integer" + }, + "name": { + "type": "string", + "maxLength": 256 + }, + "password": { + "type": "string" + }, + "port": { + "type": "integer" + }, + "rootCert": { + "type": "string" + }, + "skipVerify": { + "type": "boolean" + }, + "ssl": { + "type": "boolean" + }, + "type": { + "type": "string" + }, + "username": { + "type": "string" + }, + "version": { + "type": "string" + } + } + }, + "dto.DatabaseItem": { + "type": "object", + "properties": { + "database": { + "type": "string" + }, + "from": { + "type": "string" + }, + "id": { + "type": "integer" + }, + "name": { + "type": "string" + } + } + }, + "dto.DatabaseOption": { + "type": "object", + "properties": { + "address": { + "type": "string" + }, + "database": { + "type": "string" + }, + "from": { + "type": "string" + }, + "id": { + "type": "integer" + }, + "type": { + "type": "string" + }, + "version": { + "type": "string" + } + } + }, + "dto.DatabaseSearch": { + "type": "object", + "required": [ + "order", + "orderBy", + "page", + "pageSize" + ], + "properties": { + "info": { + "type": "string" + }, + "order": { + "type": "string", + "enum": [ + "null", + "ascending", + "descending" + ] + }, + "orderBy": { + "type": "string", + "enum": [ + "name", + "created_at" + ] + }, + "page": { + "type": "integer" + }, + "pageSize": { + "type": "integer" + }, + "type": { + "type": "string" + } + } + }, + "dto.DatabaseUpdate": { + "type": "object", + "required": [ + "type", + "username", + "version" + ], + "properties": { + "address": { + "type": "string" + }, + "clientCert": { + "type": "string" + }, + "clientKey": { + "type": "string" + }, + "description": { + "type": "string" + }, + "id": { + "type": "integer" + }, + "password": { + "type": "string" + }, + "port": { + "type": "integer" + }, + "rootCert": { + "type": "string" + }, + "skipVerify": { + "type": "boolean" + }, + "ssl": { + "type": "boolean" + }, + "type": { + "type": "string" + }, + "username": { + "type": "string" + }, + "version": { + "type": "string" + } + } + }, + "dto.DeviceBaseInfo": { + "type": "object", + "properties": { + "dns": { + "type": "array", + "items": { + "type": "string" + } + }, + "hostname": { + "type": "string" + }, + "hosts": { + "type": "array", + "items": { + "$ref": "#/definitions/dto.HostHelper" + } + }, + "localTime": { + "type": "string" + }, + "maxSize": { + "type": "integer" + }, + "ntp": { + "type": "string" + }, + "swapDetails": { + "type": "array", + "items": { + "$ref": "#/definitions/dto.SwapHelper" + } + }, + "swapMemoryAvailable": { + "type": "integer" + }, + "swapMemoryTotal": { + "type": "integer" + }, + "swapMemoryUsed": { + "type": "integer" + }, + "timeZone": { + "type": "string" + }, + "user": { + "type": "string" + } + } + }, + "dto.DiskInfo": { + "type": "object", + "properties": { + "device": { + "type": "string" + }, + "free": { + "type": "integer" + }, + "inodesFree": { + "type": "integer" + }, + "inodesTotal": { + "type": "integer" + }, + "inodesUsed": { + "type": "integer" + }, + "inodesUsedPercent": { + "type": "number" + }, + "path": { + "type": "string" + }, + "total": { + "type": "integer" + }, + "type": { + "type": "string" + }, + "used": { + "type": "integer" + }, + "usedPercent": { + "type": "number" + } + } + }, + "dto.DockerOperation": { + "type": "object", + "required": [ + "operation" + ], + "properties": { + "operation": { + "type": "string", + "enum": [ + "start", + "restart", + "stop" + ] + } + } + }, + "dto.DownloadRecord": { + "type": "object", + "required": [ + "fileDir", + "fileName", + "source" + ], + "properties": { + "fileDir": { + "type": "string" + }, + "fileName": { + "type": "string" + }, + "source": { + "type": "string", + "enum": [ + "OSS", + "S3", + "SFTP", + "MINIO", + "LOCAL", + "COS", + "KODO", + "OneDrive", + "WebDAV" + ] + } + } + }, + "dto.Fail2BanBaseInfo": { + "type": "object", + "properties": { + "banAction": { + "type": "string" + }, + "banTime": { + "type": "string" + }, + "findTime": { + "type": "string" + }, + "isActive": { + "type": "boolean" + }, + "isEnable": { + "type": "boolean" + }, + "isExist": { + "type": "boolean" + }, + "logPath": { + "type": "string" + }, + "maxRetry": { + "type": "integer" + }, + "port": { + "type": "integer" + }, + "version": { + "type": "string" + } + } + }, + "dto.Fail2BanSearch": { + "type": "object", + "required": [ + "status" + ], + "properties": { + "status": { + "type": "string", + "enum": [ + "banned", + "ignore" + ] + } + } + }, + "dto.Fail2BanUpdate": { + "type": "object", + "required": [ + "key" + ], + "properties": { + "key": { + "type": "string", + "enum": [ + "port", + "bantime", + "findtime", + "maxretry", + "banaction", + "logpath", + "port" + ] + }, + "value": { + "type": "string" + } + } + }, + "dto.FirewallBaseInfo": { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "pingStatus": { + "type": "string" + }, + "status": { + "type": "string" + }, + "version": { + "type": "string" + } + } + }, + "dto.FirewallOperation": { + "type": "object", + "required": [ + "operation" + ], + "properties": { + "operation": { + "type": "string", + "enum": [ + "start", + "stop", + "restart", + "disablePing", + "enablePing" + ] + } + } + }, + "dto.ForBuckets": { + "type": "object", + "required": [ + "credential", + "type", + "vars" + ], + "properties": { + "accessKey": { + "type": "string" + }, + "credential": { + "type": "string" + }, + "type": { + "type": "string" + }, + "vars": { + "type": "string" + } + } + }, + "dto.ForwardRuleOperate": { + "type": "object", + "properties": { + "rules": { + "type": "array", + "items": { + "type": "object", + "required": [ + "operation", + "port", + "protocol", + "targetPort" + ], + "properties": { + "num": { + "type": "string" + }, + "operation": { + "type": "string", + "enum": [ + "add", + "remove" + ] + }, + "port": { + "type": "string" + }, + "protocol": { + "type": "string", + "enum": [ + "tcp", + "udp", + "tcp/udp" + ] + }, + "targetIP": { + "type": "string" + }, + "targetPort": { + "type": "string" + } + } + } + } + } + }, + "dto.FtpBaseInfo": { + "type": "object", + "properties": { + "isActive": { + "type": "boolean" + }, + "isExist": { + "type": "boolean" + } + } + }, + "dto.FtpCreate": { + "type": "object", + "required": [ + "password", + "path", + "user" + ], + "properties": { + "description": { + "type": "string" + }, + "password": { + "type": "string" + }, + "path": { + "type": "string" + }, + "user": { + "type": "string" + } + } + }, + "dto.FtpLogSearch": { + "type": "object", + "required": [ + "page", + "pageSize" + ], + "properties": { + "operation": { + "type": "string" + }, + "page": { + "type": "integer" + }, + "pageSize": { + "type": "integer" + }, + "user": { + "type": "string" + } + } + }, + "dto.FtpUpdate": { + "type": "object", + "required": [ + "password", + "path" + ], + "properties": { + "description": { + "type": "string" + }, + "id": { + "type": "integer" + }, + "password": { + "type": "string" + }, + "path": { + "type": "string" + }, + "status": { + "type": "string" + } + } + }, + "dto.GPUInfo": { + "type": "object", + "properties": { + "fanSpeed": { + "type": "string" + }, + "gpuUtil": { + "type": "string" + }, + "index": { + "type": "integer" + }, + "maxPowerLimit": { + "type": "string" + }, + "memTotal": { + "type": "string" + }, + "memUsed": { + "type": "string" + }, + "memoryUsage": { + "type": "string" + }, + "performanceState": { + "type": "string" + }, + "powerDraw": { + "type": "string" + }, + "powerUsage": { + "type": "string" + }, + "productName": { + "type": "string" + }, + "temperature": { + "type": "string" + } + } + }, + "dto.GenerateLoad": { + "type": "object", + "required": [ + "encryptionMode" + ], + "properties": { + "encryptionMode": { + "type": "string", + "enum": [ + "rsa", + "ed25519", + "ecdsa", + "dsa" + ] + } + } + }, + "dto.GenerateSSH": { + "type": "object", + "required": [ + "encryptionMode" + ], + "properties": { + "encryptionMode": { + "type": "string", + "enum": [ + "rsa", + "ed25519", + "ecdsa", + "dsa" + ] + }, + "password": { + "type": "string" + } + } + }, + "dto.GroupCreate": { + "type": "object", + "required": [ + "name", + "type" + ], + "properties": { + "id": { + "type": "integer" + }, + "name": { + "type": "string" + }, + "type": { + "type": "string" + } + } + }, + "dto.GroupInfo": { + "type": "object", + "properties": { + "id": { + "type": "integer" + }, + "isDefault": { + "type": "boolean" + }, + "name": { + "type": "string" + }, + "type": { + "type": "string" + } + } + }, + "dto.GroupSearch": { + "type": "object", + "required": [ + "type" + ], + "properties": { + "type": { + "type": "string" + } + } + }, + "dto.GroupUpdate": { + "type": "object", + "required": [ + "type" + ], + "properties": { + "id": { + "type": "integer" + }, + "isDefault": { + "type": "boolean" + }, + "name": { + "type": "string" + }, + "type": { + "type": "string" + } + } + }, + "dto.HostConnTest": { + "type": "object", + "required": [ + "addr", + "port", + "user" + ], + "properties": { + "addr": { + "type": "string" + }, + "authMode": { + "type": "string", + "enum": [ + "password", + "key" + ] + }, + "passPhrase": { + "type": "string" + }, + "password": { + "type": "string" + }, + "port": { + "type": "integer", + "maximum": 65535, + "minimum": 1 + }, + "privateKey": { + "type": "string" + }, + "user": { + "type": "string" + } + } + }, + "dto.HostHelper": { + "type": "object", + "properties": { + "host": { + "type": "string" + }, + "ip": { + "type": "string" + } + } + }, + "dto.HostOperate": { + "type": "object", + "required": [ + "addr", + "port", + "user" + ], + "properties": { + "addr": { + "type": "string" + }, + "authMode": { + "type": "string", + "enum": [ + "password", + "key" + ] + }, + "description": { + "type": "string" + }, + "groupID": { + "type": "integer" + }, + "id": { + "type": "integer" + }, + "name": { + "type": "string" + }, + "passPhrase": { + "type": "string" + }, + "password": { + "type": "string" + }, + "port": { + "type": "integer", + "maximum": 65535, + "minimum": 1 + }, + "privateKey": { + "type": "string" + }, + "rememberPassword": { + "type": "boolean" + }, + "user": { + "type": "string" + } + } + }, + "dto.HostTree": { + "type": "object", + "properties": { + "children": { + "type": "array", + "items": { + "$ref": "#/definitions/dto.TreeChild" + } + }, + "id": { + "type": "integer" + }, + "label": { + "type": "string" + } + } + }, + "dto.ImageBuild": { + "type": "object", + "required": [ + "dockerfile", + "from", + "name" + ], + "properties": { + "dockerfile": { + "type": "string" + }, + "from": { + "type": "string" + }, + "name": { + "type": "string" + }, + "tags": { + "type": "array", + "items": { + "type": "string" + } + } + } + }, + "dto.ImageInfo": { + "type": "object", + "properties": { + "createdAt": { + "type": "string" + }, + "id": { + "type": "string" + }, + "isUsed": { + "type": "boolean" + }, + "size": { + "type": "string" + }, + "tags": { + "type": "array", + "items": { + "type": "string" + } + } + } + }, + "dto.ImageLoad": { + "type": "object", + "required": [ + "path" + ], + "properties": { + "path": { + "type": "string" + } + } + }, + "dto.ImagePull": { + "type": "object", + "required": [ + "imageName" + ], + "properties": { + "imageName": { + "type": "string" + }, + "repoID": { + "type": "integer" + } + } + }, + "dto.ImagePush": { + "type": "object", + "required": [ + "name", + "repoID", + "tagName" + ], + "properties": { + "name": { + "type": "string" + }, + "repoID": { + "type": "integer" + }, + "tagName": { + "type": "string" + } + } + }, + "dto.ImageRepoDelete": { + "type": "object", + "required": [ + "ids" + ], + "properties": { + "ids": { + "type": "array", + "items": { + "type": "integer" + } + } + } + }, + "dto.ImageRepoOption": { + "type": "object", + "properties": { + "downloadUrl": { + "type": "string" + }, + "id": { + "type": "integer" + }, + "name": { + "type": "string" + } + } + }, + "dto.ImageRepoUpdate": { + "type": "object", + "properties": { + "auth": { + "type": "boolean" + }, + "downloadUrl": { + "type": "string" + }, + "id": { + "type": "integer" + }, + "password": { + "type": "string", + "maxLength": 256 + }, + "protocol": { + "type": "string" + }, + "username": { + "type": "string", + "maxLength": 256 + } + } + }, + "dto.ImageSave": { + "type": "object", + "required": [ + "name", + "path", + "tagName" + ], + "properties": { + "name": { + "type": "string" + }, + "path": { + "type": "string" + }, + "tagName": { + "type": "string" + } + } + }, + "dto.ImageTag": { + "type": "object", + "required": [ + "sourceID", + "targetName" + ], + "properties": { + "sourceID": { + "type": "string" + }, + "targetName": { + "type": "string" + } + } + }, + "dto.InspectReq": { + "type": "object", + "required": [ + "id", + "type" + ], + "properties": { + "id": { + "type": "string" + }, + "type": { + "type": "string" + } + } + }, + "dto.LogOption": { + "type": "object", + "properties": { + "logMaxFile": { + "type": "string" + }, + "logMaxSize": { + "type": "string" + } + } + }, + "dto.Login": { + "type": "object", + "required": [ + "authMethod", + "language", + "name", + "password" + ], + "properties": { + "authMethod": { + "type": "string", + "enum": [ + "jwt", + "session" + ] + }, + "captcha": { + "type": "string" + }, + "captchaID": { + "type": "string" + }, + "ignoreCaptcha": { + "type": "boolean" + }, + "language": { + "type": "string", + "enum": [ + "zh", + "en", + "tw" + ] + }, + "name": { + "type": "string" + }, + "password": { + "type": "string" + } + } + }, + "dto.MFALogin": { + "type": "object", + "required": [ + "code", + "name", + "password" + ], + "properties": { + "authMethod": { + "type": "string" + }, + "code": { + "type": "string" + }, + "name": { + "type": "string" + }, + "password": { + "type": "string" + } + } + }, + "dto.MfaCredential": { + "type": "object", + "required": [ + "code", + "interval", + "secret" + ], + "properties": { + "code": { + "type": "string" + }, + "interval": { + "type": "string" + }, + "secret": { + "type": "string" + } + } + }, + "dto.MonitorSearch": { + "type": "object", + "required": [ + "param" + ], + "properties": { + "endTime": { + "type": "string" + }, + "info": { + "type": "string" + }, + "param": { + "type": "string", + "enum": [ + "all", + "cpu", + "memory", + "load", + "io", + "network" + ] + }, + "startTime": { + "type": "string" + } + } + }, + "dto.MysqlDBCreate": { + "type": "object", + "required": [ + "database", + "format", + "from", + "name", + "password", + "permission", + "username" + ], + "properties": { + "database": { + "type": "string" + }, + "description": { + "type": "string" + }, + "format": { + "type": "string", + "enum": [ + "utf8mb4", + "utf8", + "gbk", + "big5" + ] + }, + "from": { + "type": "string", + "enum": [ + "local", + "remote" + ] + }, + "name": { + "type": "string" + }, + "password": { + "type": "string" + }, + "permission": { + "type": "string" + }, + "username": { + "type": "string" + } + } + }, + "dto.MysqlDBDelete": { + "type": "object", + "required": [ + "database", + "id", + "type" + ], + "properties": { + "database": { + "type": "string" + }, + "deleteBackup": { + "type": "boolean" + }, + "forceDelete": { + "type": "boolean" + }, + "id": { + "type": "integer" + }, + "type": { + "type": "string", + "enum": [ + "mysql", + "mariadb" + ] + } + } + }, + "dto.MysqlDBDeleteCheck": { + "type": "object", + "required": [ + "database", + "id", + "type" + ], + "properties": { + "database": { + "type": "string" + }, + "id": { + "type": "integer" + }, + "type": { + "type": "string", + "enum": [ + "mysql", + "mariadb" + ] + } + } + }, + "dto.MysqlDBSearch": { + "type": "object", + "required": [ + "database", + "order", + "orderBy", + "page", + "pageSize" + ], + "properties": { + "database": { + "type": "string" + }, + "info": { + "type": "string" + }, + "order": { + "type": "string", + "enum": [ + "null", + "ascending", + "descending" + ] + }, + "orderBy": { + "type": "string", + "enum": [ + "name", + "created_at" + ] + }, + "page": { + "type": "integer" + }, + "pageSize": { + "type": "integer" + } + } + }, + "dto.MysqlLoadDB": { + "type": "object", + "required": [ + "database", + "from", + "type" + ], + "properties": { + "database": { + "type": "string" + }, + "from": { + "type": "string", + "enum": [ + "local", + "remote" + ] + }, + "type": { + "type": "string", + "enum": [ + "mysql", + "mariadb" + ] + } + } + }, + "dto.MysqlOption": { + "type": "object", + "properties": { + "database": { + "type": "string" + }, + "from": { + "type": "string" + }, + "id": { + "type": "integer" + }, + "name": { + "type": "string" + }, + "type": { + "type": "string" + } + } + }, + "dto.MysqlStatus": { + "type": "object", + "properties": { + "Aborted_clients": { + "type": "string" + }, + "Aborted_connects": { + "type": "string" + }, + "Bytes_received": { + "type": "string" + }, + "Bytes_sent": { + "type": "string" + }, + "Com_commit": { + "type": "string" + }, + "Com_rollback": { + "type": "string" + }, + "Connections": { + "type": "string" + }, + "Created_tmp_disk_tables": { + "type": "string" + }, + "Created_tmp_tables": { + "type": "string" + }, + "File": { + "type": "string" + }, + "Innodb_buffer_pool_pages_dirty": { + "type": "string" + }, + "Innodb_buffer_pool_read_requests": { + "type": "string" + }, + "Innodb_buffer_pool_reads": { + "type": "string" + }, + "Key_read_requests": { + "type": "string" + }, + "Key_reads": { + "type": "string" + }, + "Key_write_requests": { + "type": "string" + }, + "Key_writes": { + "type": "string" + }, + "Max_used_connections": { + "type": "string" + }, + "Open_tables": { + "type": "string" + }, + "Opened_files": { + "type": "string" + }, + "Opened_tables": { + "type": "string" + }, + "Position": { + "type": "string" + }, + "Qcache_hits": { + "type": "string" + }, + "Qcache_inserts": { + "type": "string" + }, + "Questions": { + "type": "string" + }, + "Run": { + "type": "string" + }, + "Select_full_join": { + "type": "string" + }, + "Select_range_check": { + "type": "string" + }, + "Sort_merge_passes": { + "type": "string" + }, + "Table_locks_waited": { + "type": "string" + }, + "Threads_cached": { + "type": "string" + }, + "Threads_connected": { + "type": "string" + }, + "Threads_created": { + "type": "string" + }, + "Threads_running": { + "type": "string" + }, + "Uptime": { + "type": "string" + } + } + }, + "dto.MysqlVariables": { + "type": "object", + "properties": { + "binlog_cache_size": { + "type": "string" + }, + "innodb_buffer_pool_size": { + "type": "string" + }, + "innodb_log_buffer_size": { + "type": "string" + }, + "join_buffer_size": { + "type": "string" + }, + "key_buffer_size": { + "type": "string" + }, + "long_query_time": { + "type": "string" + }, + "max_connections": { + "type": "string" + }, + "max_heap_table_size": { + "type": "string" + }, + "query_cache_size": { + "type": "string" + }, + "query_cache_type": { + "type": "string" + }, + "read_buffer_size": { + "type": "string" + }, + "read_rnd_buffer_size": { + "type": "string" + }, + "slow_query_log": { + "type": "string" + }, + "sort_buffer_size": { + "type": "string" + }, + "table_open_cache": { + "type": "string" + }, + "thread_cache_size": { + "type": "string" + }, + "thread_stack": { + "type": "string" + }, + "tmp_table_size": { + "type": "string" + } + } + }, + "dto.MysqlVariablesUpdate": { + "type": "object", + "required": [ + "database", + "type" + ], + "properties": { + "database": { + "type": "string" + }, + "type": { + "type": "string", + "enum": [ + "mysql", + "mariadb" + ] + }, + "variables": { + "type": "array", + "items": { + "$ref": "#/definitions/dto.MysqlVariablesUpdateHelper" + } + } + } + }, + "dto.MysqlVariablesUpdateHelper": { + "type": "object", + "properties": { + "param": { + "type": "string" + }, + "value": {} + } + }, + "dto.NetworkCreate": { + "type": "object", + "required": [ + "driver", + "name" + ], + "properties": { + "auxAddress": { + "type": "array", + "items": { + "$ref": "#/definitions/dto.SettingUpdate" + } + }, + "auxAddressV6": { + "type": "array", + "items": { + "$ref": "#/definitions/dto.SettingUpdate" + } + }, + "driver": { + "type": "string" + }, + "gateway": { + "type": "string" + }, + "gatewayV6": { + "type": "string" + }, + "ipRange": { + "type": "string" + }, + "ipRangeV6": { + "type": "string" + }, + "ipv4": { + "type": "boolean" + }, + "ipv6": { + "type": "boolean" + }, + "labels": { + "type": "array", + "items": { + "type": "string" + } + }, + "name": { + "type": "string" + }, + "options": { + "type": "array", + "items": { + "type": "string" + } + }, + "subnet": { + "type": "string" + }, + "subnetV6": { + "type": "string" + } + } + }, + "dto.NginxKey": { + "type": "string", + "enum": [ + "index", + "limit-conn", + "ssl", + "cache", + "http-per", + "proxy-cache" + ], + "x-enum-varnames": [ + "Index", + "LimitConn", + "SSL", + "CACHE", + "HttpPer", + "ProxyCache" + ] + }, + "dto.OneDriveInfo": { + "type": "object", + "properties": { + "client_id": { + "type": "string" + }, + "client_secret": { + "type": "string" + }, + "redirect_uri": { + "type": "string" + } + } + }, + "dto.Operate": { + "type": "object", + "required": [ + "operation" + ], + "properties": { + "operation": { + "type": "string" + } + } + }, + "dto.OperateByID": { + "type": "object", + "required": [ + "id" + ], + "properties": { + "id": { + "type": "integer" + } + } + }, + "dto.OperationWithName": { + "type": "object", + "required": [ + "name" + ], + "properties": { + "name": { + "type": "string" + } + } + }, + "dto.OperationWithNameAndType": { + "type": "object", + "required": [ + "type" + ], + "properties": { + "name": { + "type": "string" + }, + "type": { + "type": "string" + } + } + }, + "dto.Options": { + "type": "object", + "properties": { + "option": { + "type": "string" + } + } + }, + "dto.OsInfo": { + "type": "object", + "properties": { + "diskSize": { + "type": "integer" + }, + "kernelArch": { + "type": "string" + }, + "kernelVersion": { + "type": "string" + }, + "os": { + "type": "string" + }, + "platform": { + "type": "string" + }, + "platformFamily": { + "type": "string" + } + } + }, + "dto.PageContainer": { + "type": "object", + "required": [ + "order", + "orderBy", + "page", + "pageSize", + "state" + ], + "properties": { + "excludeAppStore": { + "type": "boolean" + }, + "filters": { + "type": "string" + }, + "name": { + "type": "string" + }, + "order": { + "type": "string", + "enum": [ + "null", + "ascending", + "descending" + ] + }, + "orderBy": { + "type": "string", + "enum": [ + "name", + "state", + "created_at" + ] + }, + "page": { + "type": "integer" + }, + "pageSize": { + "type": "integer" + }, + "state": { + "type": "string", + "enum": [ + "all", + "created", + "running", + "paused", + "restarting", + "removing", + "exited", + "dead" + ] + } + } + }, + "dto.PageCronjob": { + "type": "object", + "required": [ + "order", + "orderBy", + "page", + "pageSize" + ], + "properties": { + "info": { + "type": "string" + }, + "order": { + "type": "string", + "enum": [ + "null", + "ascending", + "descending" + ] + }, + "orderBy": { + "type": "string", + "enum": [ + "name", + "status", + "created_at" + ] + }, + "page": { + "type": "integer" + }, + "pageSize": { + "type": "integer" + } + } + }, + "dto.PageInfo": { + "type": "object", + "required": [ + "page", + "pageSize" + ], + "properties": { + "page": { + "type": "integer" + }, + "pageSize": { + "type": "integer" + } + } + }, + "dto.PageResult": { + "type": "object", + "properties": { + "items": {}, + "total": { + "type": "integer" + } + } + }, + "dto.PasswordUpdate": { + "type": "object", + "required": [ + "newPassword", + "oldPassword" + ], + "properties": { + "newPassword": { + "type": "string" + }, + "oldPassword": { + "type": "string" + } + } + }, + "dto.PortHelper": { + "type": "object", + "properties": { + "containerPort": { + "type": "string" + }, + "hostIP": { + "type": "string" + }, + "hostPort": { + "type": "string" + }, + "protocol": { + "type": "string" + } + } + }, + "dto.PortRuleOperate": { + "type": "object", + "required": [ + "operation", + "port", + "protocol", + "strategy" + ], + "properties": { + "address": { + "type": "string" + }, + "description": { + "type": "string" + }, + "operation": { + "type": "string", + "enum": [ + "add", + "remove" + ] + }, + "port": { + "type": "string" + }, + "protocol": { + "type": "string", + "enum": [ + "tcp", + "udp", + "tcp/udp" + ] + }, + "strategy": { + "type": "string", + "enum": [ + "accept", + "drop" + ] + } + } + }, + "dto.PortRuleUpdate": { + "type": "object", + "properties": { + "newRule": { + "$ref": "#/definitions/dto.PortRuleOperate" + }, + "oldRule": { + "$ref": "#/definitions/dto.PortRuleOperate" + } + } + }, + "dto.PortUpdate": { + "type": "object", + "required": [ + "serverPort" + ], + "properties": { + "serverPort": { + "type": "integer", + "maximum": 65535, + "minimum": 1 + } + } + }, + "dto.PostgresqlBindUser": { + "type": "object", + "required": [ + "database", + "name", + "password", + "username" + ], + "properties": { + "database": { + "type": "string" + }, + "name": { + "type": "string" + }, + "password": { + "type": "string" + }, + "superUser": { + "type": "boolean" + }, + "username": { + "type": "string" + } + } + }, + "dto.PostgresqlDBCreate": { + "type": "object", + "required": [ + "database", + "from", + "name", + "password", + "username" + ], + "properties": { + "database": { + "type": "string" + }, + "description": { + "type": "string" + }, + "format": { + "type": "string" + }, + "from": { + "type": "string", + "enum": [ + "local", + "remote" + ] + }, + "name": { + "type": "string" + }, + "password": { + "type": "string" + }, + "superUser": { + "type": "boolean" + }, + "username": { + "type": "string" + } + } + }, + "dto.PostgresqlDBDelete": { + "type": "object", + "required": [ + "database", + "id", + "type" + ], + "properties": { + "database": { + "type": "string" + }, + "deleteBackup": { + "type": "boolean" + }, + "forceDelete": { + "type": "boolean" + }, + "id": { + "type": "integer" + }, + "type": { + "type": "string", + "enum": [ + "postgresql" + ] + } + } + }, + "dto.PostgresqlDBDeleteCheck": { + "type": "object", + "required": [ + "database", + "id", + "type" + ], + "properties": { + "database": { + "type": "string" + }, + "id": { + "type": "integer" + }, + "type": { + "type": "string", + "enum": [ + "postgresql" + ] + } + } + }, + "dto.PostgresqlDBSearch": { + "type": "object", + "required": [ + "database", + "order", + "orderBy", + "page", + "pageSize" + ], + "properties": { + "database": { + "type": "string" + }, + "info": { + "type": "string" + }, + "order": { + "type": "string", + "enum": [ + "null", + "ascending", + "descending" + ] + }, + "orderBy": { + "type": "string", + "enum": [ + "name", + "created_at" + ] + }, + "page": { + "type": "integer" + }, + "pageSize": { + "type": "integer" + } + } + }, + "dto.PostgresqlLoadDB": { + "type": "object", + "required": [ + "database", + "from", + "type" + ], + "properties": { + "database": { + "type": "string" + }, + "from": { + "type": "string", + "enum": [ + "local", + "remote" + ] + }, + "type": { + "type": "string", + "enum": [ + "postgresql" + ] + } + } + }, + "dto.ProxyUpdate": { + "type": "object", + "properties": { + "proxyPasswd": { + "type": "string" + }, + "proxyPasswdKeep": { + "type": "string" + }, + "proxyPort": { + "type": "string" + }, + "proxyType": { + "type": "string" + }, + "proxyUrl": { + "type": "string" + }, + "proxyUser": { + "type": "string" + } + } + }, + "dto.RecordSearch": { + "type": "object", + "required": [ + "page", + "pageSize", + "type" + ], + "properties": { + "detailName": { + "type": "string" + }, + "name": { + "type": "string" + }, + "page": { + "type": "integer" + }, + "pageSize": { + "type": "integer" + }, + "type": { + "type": "string" + } + } + }, + "dto.RecordSearchByCronjob": { + "type": "object", + "required": [ + "cronjobID", + "page", + "pageSize" + ], + "properties": { + "cronjobID": { + "type": "integer" + }, + "page": { + "type": "integer" + }, + "pageSize": { + "type": "integer" + } + } + }, + "dto.RedisCommand": { + "type": "object", + "properties": { + "command": { + "type": "string" + }, + "id": { + "type": "integer" + }, + "name": { + "type": "string" + } + } + }, + "dto.RedisConf": { + "type": "object", + "required": [ + "database" + ], + "properties": { + "containerName": { + "type": "string" + }, + "database": { + "type": "string" + }, + "maxclients": { + "type": "string" + }, + "maxmemory": { + "type": "string" + }, + "name": { + "type": "string" + }, + "port": { + "type": "integer" + }, + "requirepass": { + "type": "string" + }, + "timeout": { + "type": "string" + } + } + }, + "dto.RedisConfPersistenceUpdate": { + "type": "object", + "required": [ + "database", + "type" + ], + "properties": { + "appendfsync": { + "type": "string" + }, + "appendonly": { + "type": "string" + }, + "database": { + "type": "string" + }, + "save": { + "type": "string" + }, + "type": { + "type": "string", + "enum": [ + "aof", + "rbd" + ] + } + } + }, + "dto.RedisConfUpdate": { + "type": "object", + "required": [ + "database" + ], + "properties": { + "database": { + "type": "string" + }, + "maxclients": { + "type": "string" + }, + "maxmemory": { + "type": "string" + }, + "timeout": { + "type": "string" + } + } + }, + "dto.RedisPersistence": { + "type": "object", + "required": [ + "database" + ], + "properties": { + "appendfsync": { + "type": "string" + }, + "appendonly": { + "type": "string" + }, + "database": { + "type": "string" + }, + "save": { + "type": "string" + } + } + }, + "dto.RedisStatus": { + "type": "object", + "required": [ + "database" + ], + "properties": { + "connected_clients": { + "type": "string" + }, + "database": { + "type": "string" + }, + "instantaneous_ops_per_sec": { + "type": "string" + }, + "keyspace_hits": { + "type": "string" + }, + "keyspace_misses": { + "type": "string" + }, + "latest_fork_usec": { + "type": "string" + }, + "mem_fragmentation_ratio": { + "type": "string" + }, + "tcp_port": { + "type": "string" + }, + "total_commands_processed": { + "type": "string" + }, + "total_connections_received": { + "type": "string" + }, + "uptime_in_days": { + "type": "string" + }, + "used_memory": { + "type": "string" + }, + "used_memory_peak": { + "type": "string" + }, + "used_memory_rss": { + "type": "string" + } + } + }, + "dto.ResourceLimit": { + "type": "object", + "properties": { + "cpu": { + "type": "integer" + }, + "memory": { + "type": "integer" + } + } + }, + "dto.RuleSearch": { + "type": "object", + "required": [ + "page", + "pageSize", + "type" + ], + "properties": { + "info": { + "type": "string" + }, + "page": { + "type": "integer" + }, + "pageSize": { + "type": "integer" + }, + "status": { + "type": "string" + }, + "strategy": { + "type": "string" + }, + "type": { + "type": "string" + } + } + }, + "dto.SSHConf": { + "type": "object", + "properties": { + "file": { + "type": "string" + } + } + }, + "dto.SSHHistory": { + "type": "object", + "properties": { + "address": { + "type": "string" + }, + "area": { + "type": "string" + }, + "authMode": { + "type": "string" + }, + "date": { + "type": "string" + }, + "dateStr": { + "type": "string" + }, + "message": { + "type": "string" + }, + "port": { + "type": "string" + }, + "status": { + "type": "string" + }, + "user": { + "type": "string" + } + } + }, + "dto.SSHInfo": { + "type": "object", + "properties": { + "autoStart": { + "type": "boolean" + }, + "listenAddress": { + "type": "string" + }, + "message": { + "type": "string" + }, + "passwordAuthentication": { + "type": "string" + }, + "permitRootLogin": { + "type": "string" + }, + "port": { + "type": "string" + }, + "pubkeyAuthentication": { + "type": "string" + }, + "status": { + "type": "string" + }, + "useDNS": { + "type": "string" + } + } + }, + "dto.SSHLog": { + "type": "object", + "properties": { + "failedCount": { + "type": "integer" + }, + "logs": { + "type": "array", + "items": { + "$ref": "#/definitions/dto.SSHHistory" + } + }, + "successfulCount": { + "type": "integer" + }, + "totalCount": { + "type": "integer" + } + } + }, + "dto.SSHUpdate": { + "type": "object", + "required": [ + "key" + ], + "properties": { + "key": { + "type": "string" + }, + "newValue": { + "type": "string" + }, + "oldValue": { + "type": "string" + } + } + }, + "dto.SSLUpdate": { + "type": "object", + "required": [ + "ssl", + "sslType" + ], + "properties": { + "cert": { + "type": "string" + }, + "domain": { + "type": "string" + }, + "key": { + "type": "string" + }, + "ssl": { + "type": "string", + "enum": [ + "enable", + "disable" + ] + }, + "sslID": { + "type": "integer" + }, + "sslType": { + "type": "string", + "enum": [ + "self", + "select", + "import", + "import-paste", + "import-local" + ] + } + } + }, + "dto.SearchForTree": { + "type": "object", + "properties": { + "info": { + "type": "string" + } + } + }, + "dto.SearchHostWithPage": { + "type": "object", + "required": [ + "page", + "pageSize" + ], + "properties": { + "groupID": { + "type": "integer" + }, + "info": { + "type": "string" + }, + "page": { + "type": "integer" + }, + "pageSize": { + "type": "integer" + } + } + }, + "dto.SearchLgLogWithPage": { + "type": "object", + "required": [ + "page", + "pageSize" + ], + "properties": { + "ip": { + "type": "string" + }, + "page": { + "type": "integer" + }, + "pageSize": { + "type": "integer" + }, + "status": { + "type": "string" + } + } + }, + "dto.SearchOpLogWithPage": { + "type": "object", + "required": [ + "page", + "pageSize" + ], + "properties": { + "operation": { + "type": "string" + }, + "page": { + "type": "integer" + }, + "pageSize": { + "type": "integer" + }, + "source": { + "type": "string" + }, + "status": { + "type": "string" + } + } + }, + "dto.SearchRecord": { + "type": "object", + "required": [ + "page", + "pageSize" + ], + "properties": { + "cronjobID": { + "type": "integer" + }, + "endTime": { + "type": "string" + }, + "page": { + "type": "integer" + }, + "pageSize": { + "type": "integer" + }, + "startTime": { + "type": "string" + }, + "status": { + "type": "string" + } + } + }, + "dto.SearchSSHLog": { + "type": "object", + "required": [ + "Status", + "page", + "pageSize" + ], + "properties": { + "Status": { + "type": "string", + "enum": [ + "Success", + "Failed", + "All" + ] + }, + "info": { + "type": "string" + }, + "page": { + "type": "integer" + }, + "pageSize": { + "type": "integer" + } + } + }, + "dto.SearchWithPage": { + "type": "object", + "required": [ + "page", + "pageSize" + ], + "properties": { + "info": { + "type": "string" + }, + "page": { + "type": "integer" + }, + "pageSize": { + "type": "integer" + } + } + }, + "dto.SettingInfo": { + "type": "object", + "properties": { + "allowIPs": { + "type": "string" + }, + "appStoreLastModified": { + "type": "string" + }, + "appStoreSyncStatus": { + "type": "string" + }, + "appStoreVersion": { + "type": "string" + }, + "bindAddress": { + "type": "string" + }, + "bindDomain": { + "type": "string" + }, + "complexityVerification": { + "type": "string" + }, + "defaultNetwork": { + "type": "string" + }, + "developerMode": { + "type": "string" + }, + "dingVars": { + "type": "string" + }, + "dockerSockPath": { + "type": "string" + }, + "email": { + "type": "string" + }, + "emailVars": { + "type": "string" + }, + "expirationDays": { + "type": "string" + }, + "expirationTime": { + "type": "string" + }, + "fileRecycleBin": { + "type": "string" + }, + "ipv6": { + "type": "string" + }, + "language": { + "type": "string" + }, + "lastCleanData": { + "type": "string" + }, + "lastCleanSize": { + "type": "string" + }, + "lastCleanTime": { + "type": "string" + }, + "localTime": { + "type": "string" + }, + "menuTabs": { + "type": "string" + }, + "messageType": { + "type": "string" + }, + "mfaInterval": { + "type": "string" + }, + "mfaSecret": { + "type": "string" + }, + "mfaStatus": { + "type": "string" + }, + "monitorInterval": { + "type": "string" + }, + "monitorStatus": { + "type": "string" + }, + "monitorStoreDays": { + "type": "string" + }, + "noAuthSetting": { + "type": "string" + }, + "ntpSite": { + "type": "string" + }, + "panelName": { + "type": "string" + }, + "port": { + "type": "string" + }, + "proxyPasswd": { + "type": "string" + }, + "proxyPasswdKeep": { + "type": "string" + }, + "proxyPort": { + "type": "string" + }, + "proxyType": { + "type": "string" + }, + "proxyUrl": { + "type": "string" + }, + "proxyUser": { + "type": "string" + }, + "securityEntrance": { + "type": "string" + }, + "serverPort": { + "type": "string" + }, + "sessionTimeout": { + "type": "string" + }, + "snapshotIgnore": { + "type": "string" + }, + "ssl": { + "type": "string" + }, + "sslType": { + "type": "string" + }, + "systemIP": { + "type": "string" + }, + "systemVersion": { + "type": "string" + }, + "theme": { + "type": "string" + }, + "timeZone": { + "type": "string" + }, + "userName": { + "type": "string" + }, + "weChatVars": { + "type": "string" + }, + "xpackHideMenu": { + "type": "string" + } + } + }, + "dto.SettingUpdate": { + "type": "object", + "required": [ + "key" + ], + "properties": { + "key": { + "type": "string" + }, + "value": { + "type": "string" + } + } + }, + "dto.SnapshotBatchDelete": { + "type": "object", + "required": [ + "ids" + ], + "properties": { + "deleteWithFile": { + "type": "boolean" + }, + "ids": { + "type": "array", + "items": { + "type": "integer" + } + } + } + }, + "dto.SnapshotCreate": { + "type": "object", + "required": [ + "defaultDownload", + "from" + ], + "properties": { + "defaultDownload": { + "type": "string" + }, + "description": { + "type": "string", + "maxLength": 256 + }, + "from": { + "type": "string" + }, + "id": { + "type": "integer" + }, + "secret": { + "type": "string" + } + } + }, + "dto.SnapshotImport": { + "type": "object", + "properties": { + "description": { + "type": "string", + "maxLength": 256 + }, + "from": { + "type": "string" + }, + "names": { + "type": "array", + "items": { + "type": "string" + } + } + } + }, + "dto.SnapshotRecover": { + "type": "object", + "required": [ + "id" + ], + "properties": { + "id": { + "type": "integer" + }, + "isNew": { + "type": "boolean" + }, + "reDownload": { + "type": "boolean" + }, + "secret": { + "type": "string" + } + } + }, + "dto.SwapHelper": { + "type": "object", + "required": [ + "path" + ], + "properties": { + "isNew": { + "type": "boolean" + }, + "path": { + "type": "string" + }, + "size": { + "type": "integer" + }, + "used": { + "type": "string" + } + } + }, + "dto.TreeChild": { + "type": "object", + "properties": { + "id": { + "type": "integer" + }, + "label": { + "type": "string" + } + } + }, + "dto.UpdateByFile": { + "type": "object", + "properties": { + "file": { + "type": "string" + } + } + }, + "dto.UpdateByNameAndFile": { + "type": "object", + "properties": { + "file": { + "type": "string" + }, + "name": { + "type": "string" + } + } + }, + "dto.UpdateDescription": { + "type": "object", + "required": [ + "id" + ], + "properties": { + "description": { + "type": "string", + "maxLength": 256 + }, + "id": { + "type": "integer" + } + } + }, + "dto.UpdateFirewallDescription": { + "type": "object", + "required": [ + "strategy" + ], + "properties": { + "address": { + "type": "string" + }, + "description": { + "type": "string" + }, + "port": { + "type": "string" + }, + "protocol": { + "type": "string" + }, + "strategy": { + "type": "string", + "enum": [ + "accept", + "drop" + ] + }, + "type": { + "type": "string" + } + } + }, + "dto.Upgrade": { + "type": "object", + "required": [ + "version" + ], + "properties": { + "version": { + "type": "string" + } + } + }, + "dto.UpgradeInfo": { + "type": "object", + "properties": { + "latestVersion": { + "type": "string" + }, + "newVersion": { + "type": "string" + }, + "releaseNote": { + "type": "string" + }, + "testVersion": { + "type": "string" + } + } + }, + "dto.UserLoginInfo": { + "type": "object", + "properties": { + "mfaStatus": { + "type": "string" + }, + "name": { + "type": "string" + }, + "token": { + "type": "string" + } + } + }, + "dto.VolumeCreate": { + "type": "object", + "required": [ + "driver", + "name" + ], + "properties": { + "driver": { + "type": "string" + }, + "labels": { + "type": "array", + "items": { + "type": "string" + } + }, + "name": { + "type": "string" + }, + "options": { + "type": "array", + "items": { + "type": "string" + } + } + } + }, + "dto.VolumeHelper": { + "type": "object", + "properties": { + "containerDir": { + "type": "string" + }, + "mode": { + "type": "string" + }, + "sourceDir": { + "type": "string" + }, + "type": { + "type": "string" + } + } + }, + "files.FileInfo": { + "type": "object", + "properties": { + "content": { + "type": "string" + }, + "extension": { + "type": "string" + }, + "favoriteID": { + "type": "integer" + }, + "gid": { + "type": "string" + }, + "group": { + "type": "string" + }, + "isDetail": { + "type": "boolean" + }, + "isDir": { + "type": "boolean" + }, + "isHidden": { + "type": "boolean" + }, + "isSymlink": { + "type": "boolean" + }, + "itemTotal": { + "type": "integer" + }, + "items": { + "type": "array", + "items": { + "$ref": "#/definitions/files.FileInfo" + } + }, + "linkPath": { + "type": "string" + }, + "mimeType": { + "type": "string" + }, + "modTime": { + "type": "string" + }, + "mode": { + "type": "string" + }, + "name": { + "type": "string" + }, + "path": { + "type": "string" + }, + "size": { + "type": "integer" + }, + "type": { + "type": "string" + }, + "uid": { + "type": "string" + }, + "updateTime": { + "type": "string" + }, + "user": { + "type": "string" + } + } + }, + "mfa.Otp": { + "type": "object", + "properties": { + "qrImage": { + "type": "string" + }, + "secret": { + "type": "string" + } + } + }, + "model.App": { + "type": "object", + "properties": { + "createdAt": { + "type": "string" + }, + "crossVersionUpdate": { + "type": "boolean" + }, + "document": { + "type": "string" + }, + "github": { + "type": "string" + }, + "icon": { + "type": "string" + }, + "id": { + "type": "integer" + }, + "key": { + "type": "string" + }, + "lastModified": { + "type": "integer" + }, + "limit": { + "type": "integer" + }, + "name": { + "type": "string" + }, + "readMe": { + "type": "string" + }, + "recommend": { + "type": "integer" + }, + "required": { + "type": "string" + }, + "resource": { + "type": "string" + }, + "shortDescEn": { + "type": "string" + }, + "shortDescZh": { + "type": "string" + }, + "status": { + "type": "string" + }, + "tags": { + "type": "array", + "items": { + "type": "string" + } + }, + "type": { + "type": "string" + }, + "updatedAt": { + "type": "string" + }, + "website": { + "type": "string" + } + } + }, + "model.AppInstall": { + "type": "object", + "properties": { + "app": { + "$ref": "#/definitions/model.App" + }, + "appDetailId": { + "type": "integer" + }, + "appId": { + "type": "integer" + }, + "containerName": { + "type": "string" + }, + "createdAt": { + "type": "string" + }, + "description": { + "type": "string" + }, + "dockerCompose": { + "type": "string" + }, + "env": { + "type": "string" + }, + "httpPort": { + "type": "integer" + }, + "httpsPort": { + "type": "integer" + }, + "id": { + "type": "integer" + }, + "message": { + "type": "string" + }, + "name": { + "type": "string" + }, + "param": { + "type": "string" + }, + "serviceName": { + "type": "string" + }, + "status": { + "type": "string" + }, + "updatedAt": { + "type": "string" + }, + "version": { + "type": "string" + } + } + }, + "model.Tag": { + "type": "object", + "properties": { + "createdAt": { + "type": "string" + }, + "id": { + "type": "integer" + }, + "key": { + "type": "string" + }, + "name": { + "type": "string" + }, + "sort": { + "type": "integer" + }, + "updatedAt": { + "type": "string" + } + } + }, + "model.Website": { + "type": "object", + "properties": { + "IPV6": { + "type": "boolean" + }, + "accessLog": { + "type": "boolean" + }, + "alias": { + "type": "string" + }, + "appInstallId": { + "type": "integer" + }, + "createdAt": { + "type": "string" + }, + "defaultServer": { + "type": "boolean" + }, + "domains": { + "type": "array", + "items": { + "$ref": "#/definitions/model.WebsiteDomain" + } + }, + "errorLog": { + "type": "boolean" + }, + "expireDate": { + "type": "string" + }, + "ftpId": { + "type": "integer" + }, + "group": { + "type": "string" + }, + "httpConfig": { + "type": "string" + }, + "id": { + "type": "integer" + }, + "primaryDomain": { + "type": "string" + }, + "protocol": { + "type": "string" + }, + "proxy": { + "type": "string" + }, + "proxyType": { + "type": "string" + }, + "remark": { + "type": "string" + }, + "rewrite": { + "type": "string" + }, + "runtimeID": { + "type": "integer" + }, + "siteDir": { + "type": "string" + }, + "status": { + "type": "string" + }, + "type": { + "type": "string" + }, + "updatedAt": { + "type": "string" + }, + "user": { + "type": "string" + }, + "webSiteGroupId": { + "type": "integer" + }, + "webSiteSSL": { + "$ref": "#/definitions/model.WebsiteSSL" + }, + "webSiteSSLId": { + "type": "integer" + } + } + }, + "model.WebsiteAcmeAccount": { + "type": "object", + "properties": { + "createdAt": { + "type": "string" + }, + "eabHmacKey": { + "type": "string" + }, + "eabKid": { + "type": "string" + }, + "email": { + "type": "string" + }, + "id": { + "type": "integer" + }, + "keyType": { + "type": "string" + }, + "type": { + "type": "string" + }, + "updatedAt": { + "type": "string" + }, + "url": { + "type": "string" + } + } + }, + "model.WebsiteDnsAccount": { + "type": "object", + "properties": { + "createdAt": { + "type": "string" + }, + "id": { + "type": "integer" + }, + "name": { + "type": "string" + }, + "type": { + "type": "string" + }, + "updatedAt": { + "type": "string" + } + } + }, + "model.WebsiteDomain": { + "type": "object", + "properties": { + "createdAt": { + "type": "string" + }, + "domain": { + "type": "string" + }, + "id": { + "type": "integer" + }, + "port": { + "type": "integer" + }, + "updatedAt": { + "type": "string" + }, + "websiteId": { + "type": "integer" + } + } + }, + "model.WebsiteSSL": { + "type": "object", + "properties": { + "acmeAccount": { + "$ref": "#/definitions/model.WebsiteAcmeAccount" + }, + "acmeAccountId": { + "type": "integer" + }, + "autoRenew": { + "type": "boolean" + }, + "caId": { + "type": "integer" + }, + "certURL": { + "type": "string" + }, + "createdAt": { + "type": "string" + }, + "description": { + "type": "string" + }, + "dir": { + "type": "string" + }, + "disableCNAME": { + "type": "boolean" + }, + "dnsAccount": { + "$ref": "#/definitions/model.WebsiteDnsAccount" + }, + "dnsAccountId": { + "type": "integer" + }, + "domains": { + "type": "string" + }, + "execShell": { + "type": "boolean" + }, + "expireDate": { + "type": "string" + }, + "id": { + "type": "integer" + }, + "keyType": { + "type": "string" + }, + "message": { + "type": "string" + }, + "nameserver1": { + "type": "string" + }, + "nameserver2": { + "type": "string" + }, + "organization": { + "type": "string" + }, + "pem": { + "type": "string" + }, + "primaryDomain": { + "type": "string" + }, + "privateKey": { + "type": "string" + }, + "provider": { + "type": "string" + }, + "pushDir": { + "type": "boolean" + }, + "shell": { + "type": "string" + }, + "skipDNS": { + "type": "boolean" + }, + "startDate": { + "type": "string" + }, + "status": { + "type": "string" + }, + "type": { + "type": "string" + }, + "updatedAt": { + "type": "string" + }, + "websites": { + "type": "array", + "items": { + "$ref": "#/definitions/model.Website" + } + } + } + }, + "request.AppInstallCreate": { + "type": "object", + "required": [ + "appDetailId", + "name" + ], + "properties": { + "advanced": { + "type": "boolean" + }, + "allowPort": { + "type": "boolean" + }, + "appDetailId": { + "type": "integer" + }, + "containerName": { + "type": "string" + }, + "cpuQuota": { + "type": "number" + }, + "dockerCompose": { + "type": "string" + }, + "editCompose": { + "type": "boolean" + }, + "hostMode": { + "type": "boolean" + }, + "memoryLimit": { + "type": "number" + }, + "memoryUnit": { + "type": "string" + }, + "name": { + "type": "string" + }, + "params": { + "type": "object", + "additionalProperties": true + }, + "pullImage": { + "type": "boolean" + }, + "services": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + } + }, + "request.AppInstalledIgnoreUpgrade": { + "type": "object", + "required": [ + "detailID", + "operate" + ], + "properties": { + "detailID": { + "type": "integer" + }, + "operate": { + "type": "string", + "enum": [ + "cancel", + "ignore" + ] + } + } + }, + "request.AppInstalledInfo": { + "type": "object", + "required": [ + "key" + ], + "properties": { + "key": { + "type": "string" + }, + "name": { + "type": "string" + } + } + }, + "request.AppInstalledOperate": { + "type": "object", + "required": [ + "installId", + "operate" + ], + "properties": { + "backup": { + "type": "boolean" + }, + "backupId": { + "type": "integer" + }, + "deleteBackup": { + "type": "boolean" + }, + "deleteDB": { + "type": "boolean" + }, + "detailId": { + "type": "integer" + }, + "dockerCompose": { + "type": "string" + }, + "forceDelete": { + "type": "boolean" + }, + "installId": { + "type": "integer" + }, + "operate": { + "type": "string" + }, + "pullImage": { + "type": "boolean" + } + } + }, + "request.AppInstalledSearch": { + "type": "object", + "required": [ + "page", + "pageSize" + ], + "properties": { + "all": { + "type": "boolean" + }, + "name": { + "type": "string" + }, + "page": { + "type": "integer" + }, + "pageSize": { + "type": "integer" + }, + "sync": { + "type": "boolean" + }, + "tags": { + "type": "array", + "items": { + "type": "string" + } + }, + "type": { + "type": "string" + }, + "unused": { + "type": "boolean" + }, + "update": { + "type": "boolean" + } + } + }, + "request.AppInstalledUpdate": { + "type": "object", + "required": [ + "installId", + "params" + ], + "properties": { + "advanced": { + "type": "boolean" + }, + "allowPort": { + "type": "boolean" + }, + "containerName": { + "type": "string" + }, + "cpuQuota": { + "type": "number" + }, + "dockerCompose": { + "type": "string" + }, + "editCompose": { + "type": "boolean" + }, + "hostMode": { + "type": "boolean" + }, + "installId": { + "type": "integer" + }, + "memoryLimit": { + "type": "number" + }, + "memoryUnit": { + "type": "string" + }, + "params": { + "type": "object", + "additionalProperties": true + }, + "pullImage": { + "type": "boolean" + } + } + }, + "request.AppSearch": { + "type": "object", + "required": [ + "page", + "pageSize" + ], + "properties": { + "name": { + "type": "string" + }, + "page": { + "type": "integer" + }, + "pageSize": { + "type": "integer" + }, + "recommend": { + "type": "boolean" + }, + "resource": { + "type": "string" + }, + "tags": { + "type": "array", + "items": { + "type": "string" + } + }, + "type": { + "type": "string" + } + } + }, + "request.DirSizeReq": { + "type": "object", + "required": [ + "path" + ], + "properties": { + "path": { + "type": "string" + } + } + }, + "request.ExposedPort": { + "type": "object", + "properties": { + "containerPort": { + "type": "integer" + }, + "hostPort": { + "type": "integer" + } + } + }, + "request.FavoriteCreate": { + "type": "object", + "required": [ + "path" + ], + "properties": { + "path": { + "type": "string" + } + } + }, + "request.FavoriteDelete": { + "type": "object", + "required": [ + "id" + ], + "properties": { + "id": { + "type": "integer" + } + } + }, + "request.FileBatchDelete": { + "type": "object", + "required": [ + "paths" + ], + "properties": { + "isDir": { + "type": "boolean" + }, + "paths": { + "type": "array", + "items": { + "type": "string" + } + } + } + }, + "request.FileCompress": { + "type": "object", + "required": [ + "dst", + "files", + "name", + "type" + ], + "properties": { + "dst": { + "type": "string" + }, + "files": { + "type": "array", + "items": { + "type": "string" + } + }, + "name": { + "type": "string" + }, + "replace": { + "type": "boolean" + }, + "secret": { + "type": "string" + }, + "type": { + "type": "string" + } + } + }, + "request.FileContentReq": { + "type": "object", + "required": [ + "path" + ], + "properties": { + "isDetail": { + "type": "boolean" + }, + "path": { + "type": "string" + } + } + }, + "request.FileCreate": { + "type": "object", + "required": [ + "path" + ], + "properties": { + "content": { + "type": "string" + }, + "isDir": { + "type": "boolean" + }, + "isLink": { + "type": "boolean" + }, + "isSymlink": { + "type": "boolean" + }, + "linkPath": { + "type": "string" + }, + "mode": { + "type": "integer" + }, + "path": { + "type": "string" + }, + "sub": { + "type": "boolean" + } + } + }, + "request.FileDeCompress": { + "type": "object", + "required": [ + "dst", + "path", + "type" + ], + "properties": { + "dst": { + "type": "string" + }, + "path": { + "type": "string" + }, + "secret": { + "type": "string" + }, + "type": { + "type": "string" + } + } + }, + "request.FileDelete": { + "type": "object", + "required": [ + "path" + ], + "properties": { + "forceDelete": { + "type": "boolean" + }, + "isDir": { + "type": "boolean" + }, + "path": { + "type": "string" + } + } + }, + "request.FileDownload": { + "type": "object", + "required": [ + "name", + "paths", + "type" + ], + "properties": { + "compress": { + "type": "boolean" + }, + "name": { + "type": "string" + }, + "paths": { + "type": "array", + "items": { + "type": "string" + } + }, + "type": { + "type": "string" + } + } + }, + "request.FileEdit": { + "type": "object", + "required": [ + "path" + ], + "properties": { + "content": { + "type": "string" + }, + "path": { + "type": "string" + } + } + }, + "request.FileMove": { + "type": "object", + "required": [ + "newPath", + "oldPaths", + "type" + ], + "properties": { + "cover": { + "type": "boolean" + }, + "name": { + "type": "string" + }, + "newPath": { + "type": "string" + }, + "oldPaths": { + "type": "array", + "items": { + "type": "string" + } + }, + "type": { + "type": "string" + } + } + }, + "request.FileOption": { + "type": "object", + "properties": { + "containSub": { + "type": "boolean" + }, + "dir": { + "type": "boolean" + }, + "expand": { + "type": "boolean" + }, + "isDetail": { + "type": "boolean" + }, + "page": { + "type": "integer" + }, + "pageSize": { + "type": "integer" + }, + "path": { + "type": "string" + }, + "search": { + "type": "string" + }, + "showHidden": { + "type": "boolean" + }, + "sortBy": { + "type": "string" + }, + "sortOrder": { + "type": "string" + } + } + }, + "request.FilePathCheck": { + "type": "object", + "required": [ + "path" + ], + "properties": { + "path": { + "type": "string" + } + } + }, + "request.FileReadByLineReq": { + "type": "object", + "required": [ + "page", + "pageSize", + "type" + ], + "properties": { + "ID": { + "type": "integer" + }, + "latest": { + "type": "boolean" + }, + "name": { + "type": "string" + }, + "page": { + "type": "integer" + }, + "pageSize": { + "type": "integer" + }, + "type": { + "type": "string" + } + } + }, + "request.FileRename": { + "type": "object", + "required": [ + "newName", + "oldName" + ], + "properties": { + "newName": { + "type": "string" + }, + "oldName": { + "type": "string" + } + } + }, + "request.FileRoleReq": { + "type": "object", + "required": [ + "group", + "mode", + "paths", + "user" + ], + "properties": { + "group": { + "type": "string" + }, + "mode": { + "type": "integer" + }, + "paths": { + "type": "array", + "items": { + "type": "string" + } + }, + "sub": { + "type": "boolean" + }, + "user": { + "type": "string" + } + } + }, + "request.FileRoleUpdate": { + "type": "object", + "required": [ + "group", + "path", + "user" + ], + "properties": { + "group": { + "type": "string" + }, + "path": { + "type": "string" + }, + "sub": { + "type": "boolean" + }, + "user": { + "type": "string" + } + } + }, + "request.FileWget": { + "type": "object", + "required": [ + "name", + "path", + "url" + ], + "properties": { + "ignoreCertificate": { + "type": "boolean" + }, + "name": { + "type": "string" + }, + "path": { + "type": "string" + }, + "url": { + "type": "string" + } + } + }, + "request.HostToolConfig": { + "type": "object", + "required": [ + "type" + ], + "properties": { + "content": { + "type": "string" + }, + "operate": { + "type": "string", + "enum": [ + "get", + "set" + ] + }, + "type": { + "type": "string", + "enum": [ + "supervisord" + ] + } + } + }, + "request.HostToolCreate": { + "type": "object", + "required": [ + "type" + ], + "properties": { + "configPath": { + "type": "string" + }, + "serviceName": { + "type": "string" + }, + "type": { + "type": "string" + } + } + }, + "request.HostToolLogReq": { + "type": "object", + "required": [ + "type" + ], + "properties": { + "type": { + "type": "string", + "enum": [ + "supervisord" + ] + } + } + }, + "request.HostToolReq": { + "type": "object", + "required": [ + "type" + ], + "properties": { + "operate": { + "type": "string", + "enum": [ + "status", + "restart", + "start", + "stop" + ] + }, + "type": { + "type": "string", + "enum": [ + "supervisord" + ] + } + } + }, + "request.NewAppInstall": { + "type": "object", + "properties": { + "advanced": { + "type": "boolean" + }, + "allowPort": { + "type": "boolean" + }, + "appDetailID": { + "type": "integer" + }, + "containerName": { + "type": "string" + }, + "cpuQuota": { + "type": "number" + }, + "dockerCompose": { + "type": "string" + }, + "editCompose": { + "type": "boolean" + }, + "hostMode": { + "type": "boolean" + }, + "memoryLimit": { + "type": "number" + }, + "memoryUnit": { + "type": "string" + }, + "name": { + "type": "string" + }, + "params": { + "type": "object", + "additionalProperties": true + }, + "pullImage": { + "type": "boolean" + } + } + }, + "request.NginxAntiLeechUpdate": { + "type": "object", + "required": [ + "extends", + "return", + "websiteID" + ], + "properties": { + "blocked": { + "type": "boolean" + }, + "cache": { + "type": "boolean" + }, + "cacheTime": { + "type": "integer" + }, + "cacheUint": { + "type": "string" + }, + "enable": { + "type": "boolean" + }, + "extends": { + "type": "string" + }, + "logEnable": { + "type": "boolean" + }, + "noneRef": { + "type": "boolean" + }, + "return": { + "type": "string" + }, + "serverNames": { + "type": "array", + "items": { + "type": "string" + } + }, + "websiteID": { + "type": "integer" + } + } + }, + "request.NginxAuthReq": { + "type": "object", + "required": [ + "websiteID" + ], + "properties": { + "websiteID": { + "type": "integer" + } + } + }, + "request.NginxAuthUpdate": { + "type": "object", + "required": [ + "operate", + "websiteID" + ], + "properties": { + "operate": { + "type": "string" + }, + "password": { + "type": "string" + }, + "remark": { + "type": "string" + }, + "username": { + "type": "string" + }, + "websiteID": { + "type": "integer" + } + } + }, + "request.NginxCommonReq": { + "type": "object", + "required": [ + "websiteID" + ], + "properties": { + "websiteID": { + "type": "integer" + } + } + }, + "request.NginxConfigFileUpdate": { + "type": "object", + "required": [ + "content" + ], + "properties": { + "backup": { + "type": "boolean" + }, + "content": { + "type": "string" + } + } + }, + "request.NginxConfigUpdate": { + "type": "object", + "required": [ + "operate" + ], + "properties": { + "operate": { + "type": "string", + "enum": [ + "add", + "update", + "delete" + ] + }, + "params": {}, + "scope": { + "$ref": "#/definitions/dto.NginxKey" + }, + "websiteId": { + "type": "integer" + } + } + }, + "request.NginxProxyUpdate": { + "type": "object", + "required": [ + "content", + "name", + "websiteID" + ], + "properties": { + "content": { + "type": "string" + }, + "name": { + "type": "string" + }, + "websiteID": { + "type": "integer" + } + } + }, + "request.NginxRedirectReq": { + "type": "object", + "required": [ + "name", + "operate", + "redirect", + "target", + "type", + "websiteID" + ], + "properties": { + "domains": { + "type": "array", + "items": { + "type": "string" + } + }, + "enable": { + "type": "boolean" + }, + "keepPath": { + "type": "boolean" + }, + "name": { + "type": "string" + }, + "operate": { + "type": "string" + }, + "path": { + "type": "string" + }, + "redirect": { + "type": "string" + }, + "redirectRoot": { + "type": "boolean" + }, + "target": { + "type": "string" + }, + "type": { + "type": "string" + }, + "websiteID": { + "type": "integer" + } + } + }, + "request.NginxRedirectUpdate": { + "type": "object", + "required": [ + "content", + "name", + "websiteID" + ], + "properties": { + "content": { + "type": "string" + }, + "name": { + "type": "string" + }, + "websiteID": { + "type": "integer" + } + } + }, + "request.NginxRewriteReq": { + "type": "object", + "required": [ + "name", + "websiteId" + ], + "properties": { + "name": { + "type": "string" + }, + "websiteId": { + "type": "integer" + } + } + }, + "request.NginxRewriteUpdate": { + "type": "object", + "required": [ + "name", + "websiteId" + ], + "properties": { + "content": { + "type": "string" + }, + "name": { + "type": "string" + }, + "websiteId": { + "type": "integer" + } + } + }, + "request.NginxScopeReq": { + "type": "object", + "required": [ + "scope" + ], + "properties": { + "scope": { + "$ref": "#/definitions/dto.NginxKey" + }, + "websiteId": { + "type": "integer" + } + } + }, + "request.NodeModuleReq": { + "type": "object", + "required": [ + "ID" + ], + "properties": { + "ID": { + "type": "integer" + } + } + }, + "request.NodePackageReq": { + "type": "object", + "properties": { + "codeDir": { + "type": "string" + } + } + }, + "request.PHPExtensionsCreate": { + "type": "object", + "required": [ + "extensions", + "name" + ], + "properties": { + "extensions": { + "type": "string" + }, + "name": { + "type": "string" + } + } + }, + "request.PHPExtensionsDelete": { + "type": "object", + "required": [ + "id" + ], + "properties": { + "id": { + "type": "integer" + } + } + }, + "request.PHPExtensionsSearch": { + "type": "object", + "required": [ + "page", + "pageSize" + ], + "properties": { + "all": { + "type": "boolean" + }, + "page": { + "type": "integer" + }, + "pageSize": { + "type": "integer" + } + } + }, + "request.PHPExtensionsUpdate": { + "type": "object", + "required": [ + "extensions", + "id" + ], + "properties": { + "extensions": { + "type": "string" + }, + "id": { + "type": "integer" + } + } + }, + "request.PortUpdate": { + "type": "object", + "properties": { + "key": { + "type": "string" + }, + "name": { + "type": "string" + }, + "port": { + "type": "integer" + } + } + }, + "request.ProcessReq": { + "type": "object", + "required": [ + "PID" + ], + "properties": { + "PID": { + "type": "integer" + } + } + }, + "request.RecycleBinReduce": { + "type": "object", + "required": [ + "from", + "rName" + ], + "properties": { + "from": { + "type": "string" + }, + "name": { + "type": "string" + }, + "rName": { + "type": "string" + } + } + }, + "request.RuntimeCreate": { + "type": "object", + "properties": { + "appDetailId": { + "type": "integer" + }, + "clean": { + "type": "boolean" + }, + "codeDir": { + "type": "string" + }, + "exposedPorts": { + "type": "array", + "items": { + "$ref": "#/definitions/request.ExposedPort" + } + }, + "image": { + "type": "string" + }, + "install": { + "type": "boolean" + }, + "name": { + "type": "string" + }, + "params": { + "type": "object", + "additionalProperties": true + }, + "port": { + "type": "integer" + }, + "resource": { + "type": "string" + }, + "source": { + "type": "string" + }, + "type": { + "type": "string" + }, + "version": { + "type": "string" + } + } + }, + "request.RuntimeDelete": { + "type": "object", + "properties": { + "forceDelete": { + "type": "boolean" + }, + "id": { + "type": "integer" + } + } + }, + "request.RuntimeOperate": { + "type": "object", + "properties": { + "ID": { + "type": "integer" + }, + "operate": { + "type": "string" + } + } + }, + "request.RuntimeSearch": { + "type": "object", + "required": [ + "page", + "pageSize" + ], + "properties": { + "name": { + "type": "string" + }, + "page": { + "type": "integer" + }, + "pageSize": { + "type": "integer" + }, + "status": { + "type": "string" + }, + "type": { + "type": "string" + } + } + }, + "request.RuntimeUpdate": { + "type": "object", + "properties": { + "clean": { + "type": "boolean" + }, + "codeDir": { + "type": "string" + }, + "exposedPorts": { + "type": "array", + "items": { + "$ref": "#/definitions/request.ExposedPort" + } + }, + "id": { + "type": "integer" + }, + "image": { + "type": "string" + }, + "install": { + "type": "boolean" + }, + "name": { + "type": "string" + }, + "params": { + "type": "object", + "additionalProperties": true + }, + "port": { + "type": "integer" + }, + "rebuild": { + "type": "boolean" + }, + "source": { + "type": "string" + }, + "version": { + "type": "string" + } + } + }, + "request.SearchUploadWithPage": { + "type": "object", + "required": [ + "page", + "pageSize", + "path" + ], + "properties": { + "page": { + "type": "integer" + }, + "pageSize": { + "type": "integer" + }, + "path": { + "type": "string" + } + } + }, + "request.SupervisorProcessConfig": { + "type": "object", + "properties": { + "command": { + "type": "string" + }, + "dir": { + "type": "string" + }, + "name": { + "type": "string" + }, + "numprocs": { + "type": "string" + }, + "operate": { + "type": "string" + }, + "user": { + "type": "string" + } + } + }, + "request.SupervisorProcessFileReq": { + "type": "object", + "required": [ + "file", + "name", + "operate" + ], + "properties": { + "content": { + "type": "string" + }, + "file": { + "type": "string", + "enum": [ + "out.log", + "err.log", + "config" + ] + }, + "name": { + "type": "string" + }, + "operate": { + "type": "string", + "enum": [ + "get", + "clear", + "update" + ] + } + } + }, + "request.WebsiteAcmeAccountCreate": { + "type": "object", + "required": [ + "email", + "keyType", + "type" + ], + "properties": { + "eabHmacKey": { + "type": "string" + }, + "eabKid": { + "type": "string" + }, + "email": { + "type": "string" + }, + "keyType": { + "type": "string", + "enum": [ + "P256", + "P384", + "2048", + "3072", + "4096", + "8192" + ] + }, + "type": { + "type": "string", + "enum": [ + "letsencrypt", + "zerossl", + "buypass", + "google" + ] + } + } + }, + "request.WebsiteBatchDelReq": { + "type": "object", + "required": [ + "ids" + ], + "properties": { + "ids": { + "type": "array", + "items": { + "type": "integer" + } + } + } + }, + "request.WebsiteCACreate": { + "type": "object", + "required": [ + "commonName", + "country", + "keyType", + "name", + "organization" + ], + "properties": { + "city": { + "type": "string" + }, + "commonName": { + "type": "string" + }, + "country": { + "type": "string" + }, + "keyType": { + "type": "string", + "enum": [ + "P256", + "P384", + "2048", + "3072", + "4096", + "8192" + ] + }, + "name": { + "type": "string" + }, + "organization": { + "type": "string" + }, + "organizationUint": { + "type": "string" + }, + "province": { + "type": "string" + } + } + }, + "request.WebsiteCAObtain": { + "type": "object", + "required": [ + "domains", + "id", + "keyType", + "time", + "unit" + ], + "properties": { + "autoRenew": { + "type": "boolean" + }, + "description": { + "type": "string" + }, + "dir": { + "type": "string" + }, + "domains": { + "type": "string" + }, + "execShell": { + "type": "boolean" + }, + "id": { + "type": "integer" + }, + "keyType": { + "type": "string", + "enum": [ + "P256", + "P384", + "2048", + "3072", + "4096", + "8192" + ] + }, + "pushDir": { + "type": "boolean" + }, + "renew": { + "type": "boolean" + }, + "shell": { + "type": "string" + }, + "sslID": { + "type": "integer" + }, + "time": { + "type": "integer" + }, + "unit": { + "type": "string" + } + } + }, + "request.WebsiteCASearch": { + "type": "object", + "required": [ + "page", + "pageSize" + ], + "properties": { + "page": { + "type": "integer" + }, + "pageSize": { + "type": "integer" + } + } + }, + "request.WebsiteCommonReq": { + "type": "object", + "required": [ + "id" + ], + "properties": { + "id": { + "type": "integer" + } + } + }, + "request.WebsiteCreate": { + "type": "object", + "required": [ + "alias", + "primaryDomain", + "type", + "webSiteGroupID" + ], + "properties": { + "IPV6": { + "type": "boolean" + }, + "alias": { + "type": "string" + }, + "appID": { + "type": "integer" + }, + "appInstall": { + "$ref": "#/definitions/request.NewAppInstall" + }, + "appInstallID": { + "type": "integer" + }, + "appType": { + "type": "string", + "enum": [ + "new", + "installed" + ] + }, + "ftpPassword": { + "type": "string" + }, + "ftpUser": { + "type": "string" + }, + "otherDomains": { + "type": "string" + }, + "port": { + "type": "integer" + }, + "primaryDomain": { + "type": "string" + }, + "proxy": { + "type": "string" + }, + "proxyType": { + "type": "string" + }, + "remark": { + "type": "string" + }, + "runtimeID": { + "type": "integer" + }, + "type": { + "type": "string" + }, + "webSiteGroupID": { + "type": "integer" + } + } + }, + "request.WebsiteDNSReq": { + "type": "object", + "required": [ + "acmeAccountId", + "domains" + ], + "properties": { + "acmeAccountId": { + "type": "integer" + }, + "domains": { + "type": "array", + "items": { + "type": "string" + } + } + } + }, + "request.WebsiteDefaultUpdate": { + "type": "object", + "properties": { + "id": { + "type": "integer" + } + } + }, + "request.WebsiteDelete": { + "type": "object", + "required": [ + "id" + ], + "properties": { + "deleteApp": { + "type": "boolean" + }, + "deleteBackup": { + "type": "boolean" + }, + "forceDelete": { + "type": "boolean" + }, + "id": { + "type": "integer" + } + } + }, + "request.WebsiteDnsAccountCreate": { + "type": "object", + "required": [ + "authorization", + "name", + "type" + ], + "properties": { + "authorization": { + "type": "object", + "additionalProperties": { + "type": "string" + } + }, + "name": { + "type": "string" + }, + "type": { + "type": "string" + } + } + }, + "request.WebsiteDnsAccountUpdate": { + "type": "object", + "required": [ + "authorization", + "id", + "name", + "type" + ], + "properties": { + "authorization": { + "type": "object", + "additionalProperties": { + "type": "string" + } + }, + "id": { + "type": "integer" + }, + "name": { + "type": "string" + }, + "type": { + "type": "string" + } + } + }, + "request.WebsiteDomainCreate": { + "type": "object", + "required": [ + "domains", + "websiteID" + ], + "properties": { + "domains": { + "type": "string" + }, + "websiteID": { + "type": "integer" + } + } + }, + "request.WebsiteDomainDelete": { + "type": "object", + "required": [ + "id" + ], + "properties": { + "id": { + "type": "integer" + } + } + }, + "request.WebsiteHTTPSOp": { + "type": "object", + "required": [ + "websiteId" + ], + "properties": { + "SSLProtocol": { + "type": "array", + "items": { + "type": "string" + } + }, + "algorithm": { + "type": "string" + }, + "certificate": { + "type": "string" + }, + "certificatePath": { + "type": "string" + }, + "enable": { + "type": "boolean" + }, + "hsts": { + "type": "boolean" + }, + "httpConfig": { + "type": "string", + "enum": [ + "HTTPSOnly", + "HTTPAlso", + "HTTPToHTTPS" + ] + }, + "importType": { + "type": "string" + }, + "privateKey": { + "type": "string" + }, + "privateKeyPath": { + "type": "string" + }, + "type": { + "type": "string", + "enum": [ + "existed", + "auto", + "manual" + ] + }, + "websiteId": { + "type": "integer" + }, + "websiteSSLId": { + "type": "integer" + } + } + }, + "request.WebsiteHtmlUpdate": { + "type": "object", + "required": [ + "content", + "type" + ], + "properties": { + "content": { + "type": "string" + }, + "type": { + "type": "string" + } + } + }, + "request.WebsiteInstallCheckReq": { + "type": "object", + "properties": { + "InstallIds": { + "type": "array", + "items": { + "type": "integer" + } + } + } + }, + "request.WebsiteLogReq": { + "type": "object", + "required": [ + "id", + "logType", + "operate" + ], + "properties": { + "id": { + "type": "integer" + }, + "logType": { + "type": "string" + }, + "operate": { + "type": "string" + }, + "page": { + "type": "integer" + }, + "pageSize": { + "type": "integer" + } + } + }, + "request.WebsiteNginxUpdate": { + "type": "object", + "required": [ + "content", + "id" + ], + "properties": { + "content": { + "type": "string" + }, + "id": { + "type": "integer" + } + } + }, + "request.WebsiteOp": { + "type": "object", + "required": [ + "id" + ], + "properties": { + "id": { + "type": "integer" + }, + "operate": { + "type": "string" + } + } + }, + "request.WebsitePHPConfigUpdate": { + "type": "object", + "required": [ + "id", + "scope" + ], + "properties": { + "disableFunctions": { + "type": "array", + "items": { + "type": "string" + } + }, + "id": { + "type": "integer" + }, + "params": { + "type": "object", + "additionalProperties": { + "type": "string" + } + }, + "scope": { + "type": "string" + }, + "uploadMaxSize": { + "type": "string" + } + } + }, + "request.WebsitePHPFileUpdate": { + "type": "object", + "required": [ + "content", + "id", + "type" + ], + "properties": { + "content": { + "type": "string" + }, + "id": { + "type": "integer" + }, + "type": { + "type": "string" + } + } + }, + "request.WebsitePHPVersionReq": { + "type": "object", + "required": [ + "runtimeID", + "websiteID" + ], + "properties": { + "retainConfig": { + "type": "boolean" + }, + "runtimeID": { + "type": "integer" + }, + "websiteID": { + "type": "integer" + } + } + }, + "request.WebsiteProxyConfig": { + "type": "object", + "required": [ + "id", + "match", + "name", + "operate", + "proxyHost", + "proxyPass" + ], + "properties": { + "cache": { + "type": "boolean" + }, + "cacheTime": { + "type": "integer" + }, + "cacheUnit": { + "type": "string" + }, + "content": { + "type": "string" + }, + "enable": { + "type": "boolean" + }, + "filePath": { + "type": "string" + }, + "id": { + "type": "integer" + }, + "match": { + "type": "string" + }, + "modifier": { + "type": "string" + }, + "name": { + "type": "string" + }, + "operate": { + "type": "string" + }, + "proxyHost": { + "type": "string" + }, + "proxyPass": { + "type": "string" + }, + "replaces": { + "type": "object", + "additionalProperties": { + "type": "string" + } + }, + "sni": { + "type": "boolean" + } + } + }, + "request.WebsiteProxyReq": { + "type": "object", + "required": [ + "id" + ], + "properties": { + "id": { + "type": "integer" + } + } + }, + "request.WebsiteResourceReq": { + "type": "object", + "required": [ + "id" + ], + "properties": { + "id": { + "type": "integer" + } + } + }, + "request.WebsiteSSLApply": { + "type": "object", + "required": [ + "ID" + ], + "properties": { + "ID": { + "type": "integer" + }, + "nameservers": { + "type": "array", + "items": { + "type": "string" + } + }, + "skipDNSCheck": { + "type": "boolean" + } + } + }, + "request.WebsiteSSLCreate": { + "type": "object", + "required": [ + "acmeAccountId", + "primaryDomain", + "provider" + ], + "properties": { + "acmeAccountId": { + "type": "integer" + }, + "apply": { + "type": "boolean" + }, + "autoRenew": { + "type": "boolean" + }, + "description": { + "type": "string" + }, + "dir": { + "type": "string" + }, + "disableCNAME": { + "type": "boolean" + }, + "dnsAccountId": { + "type": "integer" + }, + "execShell": { + "type": "boolean" + }, + "id": { + "type": "integer" + }, + "keyType": { + "type": "string" + }, + "nameserver1": { + "type": "string" + }, + "nameserver2": { + "type": "string" + }, + "otherDomains": { + "type": "string" + }, + "primaryDomain": { + "type": "string" + }, + "provider": { + "type": "string" + }, + "pushDir": { + "type": "boolean" + }, + "shell": { + "type": "string" + }, + "skipDNS": { + "type": "boolean" + } + } + }, + "request.WebsiteSSLSearch": { + "type": "object", + "required": [ + "page", + "pageSize" + ], + "properties": { + "acmeAccountID": { + "type": "string" + }, + "page": { + "type": "integer" + }, + "pageSize": { + "type": "integer" + } + } + }, + "request.WebsiteSSLUpdate": { + "type": "object", + "required": [ + "id", + "primaryDomain", + "provider" + ], + "properties": { + "acmeAccountId": { + "type": "integer" + }, + "apply": { + "type": "boolean" + }, + "autoRenew": { + "type": "boolean" + }, + "description": { + "type": "string" + }, + "dir": { + "type": "string" + }, + "disableCNAME": { + "type": "boolean" + }, + "dnsAccountId": { + "type": "integer" + }, + "execShell": { + "type": "boolean" + }, + "id": { + "type": "integer" + }, + "keyType": { + "type": "string" + }, + "nameserver1": { + "type": "string" + }, + "nameserver2": { + "type": "string" + }, + "otherDomains": { + "type": "string" + }, + "primaryDomain": { + "type": "string" + }, + "provider": { + "type": "string" + }, + "pushDir": { + "type": "boolean" + }, + "shell": { + "type": "string" + }, + "skipDNS": { + "type": "boolean" + } + } + }, + "request.WebsiteSSLUpload": { + "type": "object", + "required": [ + "type" + ], + "properties": { + "certificate": { + "type": "string" + }, + "certificatePath": { + "type": "string" + }, + "description": { + "type": "string" + }, + "privateKey": { + "type": "string" + }, + "privateKeyPath": { + "type": "string" + }, + "sslID": { + "type": "integer" + }, + "type": { + "type": "string", + "enum": [ + "paste", + "local" + ] + } + } + }, + "request.WebsiteSearch": { + "type": "object", + "required": [ + "order", + "orderBy", + "page", + "pageSize" + ], + "properties": { + "name": { + "type": "string" + }, + "order": { + "type": "string", + "enum": [ + "null", + "ascending", + "descending" + ] + }, + "orderBy": { + "type": "string", + "enum": [ + "primary_domain", + "type", + "status", + "created_at", + "expire_date" + ] + }, + "page": { + "type": "integer" + }, + "pageSize": { + "type": "integer" + }, + "websiteGroupId": { + "type": "integer" + } + } + }, + "request.WebsiteUpdate": { + "type": "object", + "required": [ + "id", + "primaryDomain" + ], + "properties": { + "IPV6": { + "type": "boolean" + }, + "expireDate": { + "type": "string" + }, + "id": { + "type": "integer" + }, + "primaryDomain": { + "type": "string" + }, + "remark": { + "type": "string" + }, + "webSiteGroupID": { + "type": "integer" + } + } + }, + "request.WebsiteUpdateDir": { + "type": "object", + "required": [ + "id", + "siteDir" + ], + "properties": { + "id": { + "type": "integer" + }, + "siteDir": { + "type": "string" + } + } + }, + "request.WebsiteUpdateDirPermission": { + "type": "object", + "required": [ + "group", + "id", + "user" + ], + "properties": { + "group": { + "type": "string" + }, + "id": { + "type": "integer" + }, + "user": { + "type": "string" + } + } + }, + "response.AppDTO": { + "type": "object", + "properties": { + "createdAt": { + "type": "string" + }, + "crossVersionUpdate": { + "type": "boolean" + }, + "document": { + "type": "string" + }, + "github": { + "type": "string" + }, + "icon": { + "type": "string" + }, + "id": { + "type": "integer" + }, + "installed": { + "type": "boolean" + }, + "key": { + "type": "string" + }, + "lastModified": { + "type": "integer" + }, + "limit": { + "type": "integer" + }, + "name": { + "type": "string" + }, + "readMe": { + "type": "string" + }, + "recommend": { + "type": "integer" + }, + "required": { + "type": "string" + }, + "resource": { + "type": "string" + }, + "shortDescEn": { + "type": "string" + }, + "shortDescZh": { + "type": "string" + }, + "status": { + "type": "string" + }, + "tags": { + "type": "array", + "items": { + "$ref": "#/definitions/model.Tag" + } + }, + "type": { + "type": "string" + }, + "updatedAt": { + "type": "string" + }, + "versions": { + "type": "array", + "items": { + "type": "string" + } + }, + "website": { + "type": "string" + } + } + }, + "response.AppDetailDTO": { + "type": "object", + "properties": { + "appId": { + "type": "integer" + }, + "createdAt": { + "type": "string" + }, + "dockerCompose": { + "type": "string" + }, + "downloadCallBackUrl": { + "type": "string" + }, + "downloadUrl": { + "type": "string" + }, + "enable": { + "type": "boolean" + }, + "hostMode": { + "type": "boolean" + }, + "id": { + "type": "integer" + }, + "ignoreUpgrade": { + "type": "boolean" + }, + "image": { + "type": "string" + }, + "lastModified": { + "type": "integer" + }, + "lastVersion": { + "type": "string" + }, + "params": {}, + "status": { + "type": "string" + }, + "update": { + "type": "boolean" + }, + "updatedAt": { + "type": "string" + }, + "version": { + "type": "string" + } + } + }, + "response.AppInstalledCheck": { + "type": "object", + "properties": { + "app": { + "type": "string" + }, + "appInstallId": { + "type": "integer" + }, + "containerName": { + "type": "string" + }, + "createdAt": { + "type": "string" + }, + "httpPort": { + "type": "integer" + }, + "httpsPort": { + "type": "integer" + }, + "installPath": { + "type": "string" + }, + "isExist": { + "type": "boolean" + }, + "lastBackupAt": { + "type": "string" + }, + "name": { + "type": "string" + }, + "status": { + "type": "string" + }, + "version": { + "type": "string" + } + } + }, + "response.AppParam": { + "type": "object", + "properties": { + "edit": { + "type": "boolean" + }, + "key": { + "type": "string" + }, + "labelEn": { + "type": "string" + }, + "labelZh": { + "type": "string" + }, + "multiple": { + "type": "boolean" + }, + "required": { + "type": "boolean" + }, + "rule": { + "type": "string" + }, + "showValue": { + "type": "string" + }, + "type": { + "type": "string" + }, + "value": {}, + "values": {} + } + }, + "response.AppService": { + "type": "object", + "properties": { + "config": {}, + "from": { + "type": "string" + }, + "label": { + "type": "string" + }, + "value": { + "type": "string" + } + } + }, + "response.FileInfo": { + "type": "object", + "properties": { + "content": { + "type": "string" + }, + "extension": { + "type": "string" + }, + "favoriteID": { + "type": "integer" + }, + "gid": { + "type": "string" + }, + "group": { + "type": "string" + }, + "isDetail": { + "type": "boolean" + }, + "isDir": { + "type": "boolean" + }, + "isHidden": { + "type": "boolean" + }, + "isSymlink": { + "type": "boolean" + }, + "itemTotal": { + "type": "integer" + }, + "items": { + "type": "array", + "items": { + "$ref": "#/definitions/files.FileInfo" + } + }, + "linkPath": { + "type": "string" + }, + "mimeType": { + "type": "string" + }, + "modTime": { + "type": "string" + }, + "mode": { + "type": "string" + }, + "name": { + "type": "string" + }, + "path": { + "type": "string" + }, + "size": { + "type": "integer" + }, + "type": { + "type": "string" + }, + "uid": { + "type": "string" + }, + "updateTime": { + "type": "string" + }, + "user": { + "type": "string" + } + } + }, + "response.FileTree": { + "type": "object", + "properties": { + "children": { + "type": "array", + "items": { + "$ref": "#/definitions/response.FileTree" + } + }, + "extension": { + "type": "string" + }, + "id": { + "type": "string" + }, + "isDir": { + "type": "boolean" + }, + "name": { + "type": "string" + }, + "path": { + "type": "string" + } + } + }, + "response.IgnoredApp": { + "type": "object", + "properties": { + "detailID": { + "type": "integer" + }, + "icon": { + "type": "string" + }, + "name": { + "type": "string" + }, + "version": { + "type": "string" + } + } + }, + "response.NginxParam": { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "params": { + "type": "array", + "items": { + "type": "string" + } + } + } + }, + "response.NginxStatus": { + "type": "object", + "properties": { + "accepts": { + "type": "string" + }, + "active": { + "type": "string" + }, + "handled": { + "type": "string" + }, + "reading": { + "type": "string" + }, + "requests": { + "type": "string" + }, + "waiting": { + "type": "string" + }, + "writing": { + "type": "string" + } + } + }, + "response.PHPConfig": { + "type": "object", + "properties": { + "disableFunctions": { + "type": "array", + "items": { + "type": "string" + } + }, + "params": { + "type": "object", + "additionalProperties": { + "type": "string" + } + }, + "uploadMaxSize": { + "type": "string" + } + } + }, + "response.PHPExtensionsDTO": { + "type": "object", + "properties": { + "createdAt": { + "type": "string" + }, + "extensions": { + "type": "string" + }, + "id": { + "type": "integer" + }, + "name": { + "type": "string" + }, + "updatedAt": { + "type": "string" + } + } + }, + "response.WebsiteAcmeAccountDTO": { + "type": "object", + "properties": { + "createdAt": { + "type": "string" + }, + "eabHmacKey": { + "type": "string" + }, + "eabKid": { + "type": "string" + }, + "email": { + "type": "string" + }, + "id": { + "type": "integer" + }, + "keyType": { + "type": "string" + }, + "type": { + "type": "string" + }, + "updatedAt": { + "type": "string" + }, + "url": { + "type": "string" + } + } + }, + "response.WebsiteCADTO": { + "type": "object", + "properties": { + "city": { + "type": "string" + }, + "commonName": { + "type": "string" + }, + "country": { + "type": "string" + }, + "createdAt": { + "type": "string" + }, + "csr": { + "type": "string" + }, + "id": { + "type": "integer" + }, + "keyType": { + "type": "string" + }, + "name": { + "type": "string" + }, + "organization": { + "type": "string" + }, + "organizationUint": { + "type": "string" + }, + "privateKey": { + "type": "string" + }, + "province": { + "type": "string" + }, + "updatedAt": { + "type": "string" + } + } + }, + "response.WebsiteDNSRes": { + "type": "object", + "properties": { + "domain": { + "type": "string" + }, + "err": { + "type": "string" + }, + "resolve": { + "type": "string" + }, + "value": { + "type": "string" + } + } + }, + "response.WebsiteDTO": { + "type": "object", + "properties": { + "IPV6": { + "type": "boolean" + }, + "accessLog": { + "type": "boolean" + }, + "accessLogPath": { + "type": "string" + }, + "alias": { + "type": "string" + }, + "appInstallId": { + "type": "integer" + }, + "appName": { + "type": "string" + }, + "createdAt": { + "type": "string" + }, + "defaultServer": { + "type": "boolean" + }, + "domains": { + "type": "array", + "items": { + "$ref": "#/definitions/model.WebsiteDomain" + } + }, + "errorLog": { + "type": "boolean" + }, + "errorLogPath": { + "type": "string" + }, + "expireDate": { + "type": "string" + }, + "ftpId": { + "type": "integer" + }, + "group": { + "type": "string" + }, + "httpConfig": { + "type": "string" + }, + "id": { + "type": "integer" + }, + "primaryDomain": { + "type": "string" + }, + "protocol": { + "type": "string" + }, + "proxy": { + "type": "string" + }, + "proxyType": { + "type": "string" + }, + "remark": { + "type": "string" + }, + "rewrite": { + "type": "string" + }, + "runtimeID": { + "type": "integer" + }, + "runtimeName": { + "type": "string" + }, + "siteDir": { + "type": "string" + }, + "sitePath": { + "type": "string" + }, + "status": { + "type": "string" + }, + "type": { + "type": "string" + }, + "updatedAt": { + "type": "string" + }, + "user": { + "type": "string" + }, + "webSiteGroupId": { + "type": "integer" + }, + "webSiteSSL": { + "$ref": "#/definitions/model.WebsiteSSL" + }, + "webSiteSSLId": { + "type": "integer" + } + } + }, + "response.WebsiteHTTPS": { + "type": "object", + "properties": { + "SSL": { + "$ref": "#/definitions/model.WebsiteSSL" + }, + "SSLProtocol": { + "type": "array", + "items": { + "type": "string" + } + }, + "algorithm": { + "type": "string" + }, + "enable": { + "type": "boolean" + }, + "hsts": { + "type": "boolean" + }, + "httpConfig": { + "type": "string" + } + } + }, + "response.WebsiteLog": { + "type": "object", + "properties": { + "content": { + "type": "string" + }, + "enable": { + "type": "boolean" + }, + "end": { + "type": "boolean" + }, + "path": { + "type": "string" + } + } + }, + "response.WebsiteNginxConfig": { + "type": "object", + "properties": { + "enable": { + "type": "boolean" + }, + "params": { + "type": "array", + "items": { + "$ref": "#/definitions/response.NginxParam" + } + } + } + }, + "response.WebsitePreInstallCheck": { + "type": "object", + "properties": { + "appName": { + "type": "string" + }, + "name": { + "type": "string" + }, + "status": { + "type": "string" + }, + "version": { + "type": "string" + } + } + } + } +} \ No newline at end of file diff --git a/agent/cmd/server/docs/swagger.yaml b/agent/cmd/server/docs/swagger.yaml new file mode 100644 index 000000000..9dabd1965 --- /dev/null +++ b/agent/cmd/server/docs/swagger.yaml @@ -0,0 +1,15166 @@ +basePath: /api/v1 +definitions: + dto.AddrRuleOperate: + properties: + address: + type: string + description: + type: string + operation: + enum: + - add + - remove + type: string + strategy: + enum: + - accept + - drop + type: string + required: + - address + - operation + - strategy + type: object + dto.AddrRuleUpdate: + properties: + newRule: + $ref: '#/definitions/dto.AddrRuleOperate' + oldRule: + $ref: '#/definitions/dto.AddrRuleOperate' + type: object + dto.AppInstallInfo: + properties: + id: + type: integer + key: + type: string + name: + type: string + type: object + dto.AppResource: + properties: + name: + type: string + type: + type: string + type: object + dto.AppVersion: + properties: + detailId: + type: integer + dockerCompose: + type: string + version: + type: string + type: object + dto.BackupInfo: + properties: + backupPath: + type: string + bucket: + type: string + createdAt: + type: string + id: + type: integer + type: + type: string + vars: + type: string + type: object + dto.BackupOperate: + properties: + accessKey: + type: string + backupPath: + type: string + bucket: + type: string + credential: + type: string + id: + type: integer + type: + type: string + vars: + type: string + required: + - type + - vars + type: object + dto.BackupSearchFile: + properties: + type: + type: string + required: + - type + type: object + dto.BatchDelete: + properties: + force: + type: boolean + names: + items: + type: string + type: array + required: + - names + type: object + dto.BatchDeleteReq: + properties: + ids: + items: + type: integer + type: array + required: + - ids + type: object + dto.BatchRuleOperate: + properties: + rules: + items: + $ref: '#/definitions/dto.PortRuleOperate' + type: array + type: + type: string + required: + - type + type: object + dto.BindInfo: + properties: + bindAddress: + type: string + ipv6: + enum: + - enable + - disable + type: string + required: + - bindAddress + - ipv6 + type: object + dto.BindUser: + properties: + database: + type: string + db: + type: string + password: + type: string + permission: + type: string + username: + type: string + required: + - database + - db + - password + - permission + - username + type: object + dto.CaptchaResponse: + properties: + captchaID: + type: string + imagePath: + type: string + type: object + dto.ChangeDBInfo: + properties: + database: + type: string + from: + enum: + - local + - remote + type: string + id: + type: integer + type: + enum: + - mysql + - mariadb + - postgresql + type: string + value: + type: string + required: + - database + - from + - type + - value + type: object + dto.ChangeHostGroup: + properties: + groupID: + type: integer + id: + type: integer + required: + - groupID + - id + type: object + dto.ChangePasswd: + properties: + passwd: + type: string + user: + type: string + type: object + dto.ChangeRedisPass: + properties: + database: + type: string + value: + type: string + required: + - database + type: object + dto.ClamBaseInfo: + properties: + freshIsActive: + type: boolean + freshIsExist: + type: boolean + freshVersion: + type: string + isActive: + type: boolean + isExist: + type: boolean + version: + type: string + type: object + dto.ClamCreate: + properties: + description: + type: string + infectedDir: + type: string + infectedStrategy: + type: string + name: + type: string + path: + type: string + spec: + type: string + status: + type: string + type: object + dto.ClamDelete: + properties: + ids: + items: + type: integer + type: array + removeInfected: + type: boolean + removeRecord: + type: boolean + required: + - ids + type: object + dto.ClamFileReq: + properties: + name: + type: string + tail: + type: string + required: + - name + type: object + dto.ClamLogReq: + properties: + clamName: + type: string + recordName: + type: string + tail: + type: string + type: object + dto.ClamLogSearch: + properties: + clamID: + type: integer + endTime: + type: string + page: + type: integer + pageSize: + type: integer + startTime: + type: string + required: + - page + - pageSize + type: object + dto.ClamUpdate: + properties: + description: + type: string + id: + type: integer + infectedDir: + type: string + infectedStrategy: + type: string + name: + type: string + path: + type: string + spec: + type: string + type: object + dto.ClamUpdateStatus: + properties: + id: + type: integer + status: + type: string + type: object + dto.Clean: + properties: + name: + type: string + size: + type: integer + treeType: + type: string + type: object + dto.CleanLog: + properties: + logType: + enum: + - login + - operation + type: string + required: + - logType + type: object + dto.CommandInfo: + properties: + command: + type: string + groupBelong: + type: string + groupID: + type: integer + id: + type: integer + name: + type: string + type: object + dto.CommandOperate: + properties: + command: + type: string + groupBelong: + type: string + groupID: + type: integer + id: + type: integer + name: + type: string + required: + - command + - name + type: object + dto.CommonBackup: + properties: + detailName: + type: string + name: + type: string + secret: + type: string + type: + enum: + - app + - mysql + - mariadb + - redis + - website + - postgresql + type: string + required: + - type + type: object + dto.CommonRecover: + properties: + detailName: + type: string + file: + type: string + name: + type: string + secret: + type: string + source: + enum: + - OSS + - S3 + - SFTP + - MINIO + - LOCAL + - COS + - KODO + - OneDrive + - WebDAV + type: string + type: + enum: + - app + - mysql + - mariadb + - redis + - website + - postgresql + type: string + required: + - source + - type + type: object + dto.ComposeCreate: + properties: + file: + type: string + from: + enum: + - edit + - path + - template + type: string + name: + type: string + path: + type: string + template: + type: integer + required: + - from + type: object + dto.ComposeOperation: + properties: + name: + type: string + operation: + enum: + - start + - stop + - down + type: string + path: + type: string + withFile: + type: boolean + required: + - name + - operation + - path + type: object + dto.ComposeTemplateCreate: + properties: + content: + type: string + description: + type: string + name: + type: string + required: + - name + type: object + dto.ComposeTemplateInfo: + properties: + content: + type: string + createdAt: + type: string + description: + type: string + id: + type: integer + name: + type: string + type: object + dto.ComposeTemplateUpdate: + properties: + content: + type: string + description: + type: string + id: + type: integer + type: object + dto.ComposeUpdate: + properties: + content: + type: string + name: + type: string + path: + type: string + required: + - content + - name + - path + type: object + dto.ContainerCommit: + properties: + author: + type: string + comment: + type: string + containerID: + type: string + containerName: + type: string + newImageName: + type: string + pause: + type: boolean + required: + - containerID + type: object + dto.ContainerListStats: + properties: + containerID: + type: string + cpuPercent: + type: number + cpuTotalUsage: + type: integer + memoryCache: + type: integer + memoryLimit: + type: integer + memoryPercent: + type: number + memoryUsage: + type: integer + percpuUsage: + type: integer + systemUsage: + type: integer + type: object + dto.ContainerOperate: + properties: + autoRemove: + type: boolean + cmd: + items: + type: string + type: array + containerID: + type: string + cpuShares: + type: integer + entrypoint: + items: + type: string + type: array + env: + items: + type: string + type: array + exposedPorts: + items: + $ref: '#/definitions/dto.PortHelper' + type: array + forcePull: + type: boolean + image: + type: string + ipv4: + type: string + ipv6: + type: string + labels: + items: + type: string + type: array + memory: + type: number + name: + type: string + nanoCPUs: + type: number + network: + type: string + openStdin: + type: boolean + privileged: + type: boolean + publishAllPorts: + type: boolean + restartPolicy: + type: string + tty: + type: boolean + volumes: + items: + $ref: '#/definitions/dto.VolumeHelper' + type: array + required: + - image + - name + type: object + dto.ContainerOperation: + properties: + names: + items: + type: string + type: array + operation: + enum: + - start + - stop + - restart + - kill + - pause + - unpause + - remove + type: string + required: + - names + - operation + type: object + dto.ContainerPrune: + properties: + pruneType: + enum: + - container + - image + - volume + - network + - buildcache + type: string + withTagAll: + type: boolean + required: + - pruneType + type: object + dto.ContainerPruneReport: + properties: + deletedNumber: + type: integer + spaceReclaimed: + type: integer + type: object + dto.ContainerRename: + properties: + name: + type: string + newName: + type: string + required: + - name + - newName + type: object + dto.ContainerStats: + properties: + cache: + type: number + cpuPercent: + type: number + ioRead: + type: number + ioWrite: + type: number + memory: + type: number + networkRX: + type: number + networkTX: + type: number + shotTime: + type: string + type: object + dto.ContainerUpgrade: + properties: + forcePull: + type: boolean + image: + type: string + name: + type: string + required: + - image + - name + type: object + dto.CronjobBatchDelete: + properties: + cleanData: + type: boolean + ids: + items: + type: integer + type: array + required: + - ids + type: object + dto.CronjobClean: + properties: + cleanData: + type: boolean + cronjobID: + type: integer + isDelete: + type: boolean + required: + - cronjobID + type: object + dto.CronjobCreate: + properties: + appID: + type: string + backupAccounts: + type: string + command: + type: string + containerName: + type: string + dbName: + type: string + dbType: + type: string + defaultDownload: + type: string + exclusionRules: + type: string + name: + type: string + retainCopies: + minimum: 1 + type: integer + script: + type: string + secret: + type: string + sourceDir: + type: string + spec: + type: string + type: + type: string + url: + type: string + website: + type: string + required: + - name + - spec + - type + type: object + dto.CronjobDownload: + properties: + backupAccountID: + type: integer + recordID: + type: integer + required: + - backupAccountID + - recordID + type: object + dto.CronjobUpdate: + properties: + appID: + type: string + backupAccounts: + type: string + command: + type: string + containerName: + type: string + dbName: + type: string + dbType: + type: string + defaultDownload: + type: string + exclusionRules: + type: string + id: + type: integer + name: + type: string + retainCopies: + minimum: 1 + type: integer + script: + type: string + secret: + type: string + sourceDir: + type: string + spec: + type: string + url: + type: string + website: + type: string + required: + - id + - name + - spec + type: object + dto.CronjobUpdateStatus: + properties: + id: + type: integer + status: + type: string + required: + - id + - status + type: object + dto.DBBaseInfo: + properties: + containerName: + type: string + name: + type: string + port: + type: integer + type: object + dto.DBConfUpdateByFile: + properties: + database: + type: string + file: + type: string + type: + enum: + - mysql + - mariadb + - postgresql + - redis + type: string + required: + - database + - type + type: object + dto.DaemonJsonConf: + properties: + cgroupDriver: + type: string + experimental: + type: boolean + fixedCidrV6: + type: string + insecureRegistries: + items: + type: string + type: array + ip6Tables: + type: boolean + iptables: + type: boolean + ipv6: + type: boolean + isSwarm: + type: boolean + liveRestore: + type: boolean + logMaxFile: + type: string + logMaxSize: + type: string + registryMirrors: + items: + type: string + type: array + status: + type: string + version: + type: string + type: object + dto.DaemonJsonUpdateByFile: + properties: + file: + type: string + type: object + dto.DashboardBase: + properties: + appInstalledNumber: + type: integer + cpuCores: + type: integer + cpuLogicalCores: + type: integer + cpuModelName: + type: string + cronjobNumber: + type: integer + currentInfo: + $ref: '#/definitions/dto.DashboardCurrent' + databaseNumber: + type: integer + hostname: + type: string + kernelArch: + type: string + kernelVersion: + type: string + os: + type: string + platform: + type: string + platformFamily: + type: string + platformVersion: + type: string + virtualizationSystem: + type: string + websiteNumber: + type: integer + type: object + dto.DashboardCurrent: + properties: + cpuPercent: + items: + type: number + type: array + cpuTotal: + type: integer + cpuUsed: + type: number + cpuUsedPercent: + type: number + diskData: + items: + $ref: '#/definitions/dto.DiskInfo' + type: array + gpuData: + items: + $ref: '#/definitions/dto.GPUInfo' + type: array + ioCount: + type: integer + ioReadBytes: + type: integer + ioReadTime: + type: integer + ioWriteBytes: + type: integer + ioWriteTime: + type: integer + load1: + type: number + load5: + type: number + load15: + type: number + loadUsagePercent: + type: number + memoryAvailable: + type: integer + memoryTotal: + type: integer + memoryUsed: + type: integer + memoryUsedPercent: + type: number + netBytesRecv: + type: integer + netBytesSent: + type: integer + procs: + type: integer + shotTime: + type: string + swapMemoryAvailable: + type: integer + swapMemoryTotal: + type: integer + swapMemoryUsed: + type: integer + swapMemoryUsedPercent: + type: number + timeSinceUptime: + type: string + uptime: + type: integer + type: object + dto.DatabaseCreate: + properties: + address: + type: string + clientCert: + type: string + clientKey: + type: string + description: + type: string + from: + enum: + - local + - remote + type: string + name: + maxLength: 256 + type: string + password: + type: string + port: + type: integer + rootCert: + type: string + skipVerify: + type: boolean + ssl: + type: boolean + type: + type: string + username: + type: string + version: + type: string + required: + - from + - name + - type + - username + - version + type: object + dto.DatabaseDelete: + properties: + deleteBackup: + type: boolean + forceDelete: + type: boolean + id: + type: integer + required: + - id + type: object + dto.DatabaseInfo: + properties: + address: + type: string + clientCert: + type: string + clientKey: + type: string + createdAt: + type: string + description: + type: string + from: + type: string + id: + type: integer + name: + maxLength: 256 + type: string + password: + type: string + port: + type: integer + rootCert: + type: string + skipVerify: + type: boolean + ssl: + type: boolean + type: + type: string + username: + type: string + version: + type: string + type: object + dto.DatabaseItem: + properties: + database: + type: string + from: + type: string + id: + type: integer + name: + type: string + type: object + dto.DatabaseOption: + properties: + address: + type: string + database: + type: string + from: + type: string + id: + type: integer + type: + type: string + version: + type: string + type: object + dto.DatabaseSearch: + properties: + info: + type: string + order: + enum: + - "null" + - ascending + - descending + type: string + orderBy: + enum: + - name + - created_at + type: string + page: + type: integer + pageSize: + type: integer + type: + type: string + required: + - order + - orderBy + - page + - pageSize + type: object + dto.DatabaseUpdate: + properties: + address: + type: string + clientCert: + type: string + clientKey: + type: string + description: + type: string + id: + type: integer + password: + type: string + port: + type: integer + rootCert: + type: string + skipVerify: + type: boolean + ssl: + type: boolean + type: + type: string + username: + type: string + version: + type: string + required: + - type + - username + - version + type: object + dto.DeviceBaseInfo: + properties: + dns: + items: + type: string + type: array + hostname: + type: string + hosts: + items: + $ref: '#/definitions/dto.HostHelper' + type: array + localTime: + type: string + maxSize: + type: integer + ntp: + type: string + swapDetails: + items: + $ref: '#/definitions/dto.SwapHelper' + type: array + swapMemoryAvailable: + type: integer + swapMemoryTotal: + type: integer + swapMemoryUsed: + type: integer + timeZone: + type: string + user: + type: string + type: object + dto.DiskInfo: + properties: + device: + type: string + free: + type: integer + inodesFree: + type: integer + inodesTotal: + type: integer + inodesUsed: + type: integer + inodesUsedPercent: + type: number + path: + type: string + total: + type: integer + type: + type: string + used: + type: integer + usedPercent: + type: number + type: object + dto.DockerOperation: + properties: + operation: + enum: + - start + - restart + - stop + type: string + required: + - operation + type: object + dto.DownloadRecord: + properties: + fileDir: + type: string + fileName: + type: string + source: + enum: + - OSS + - S3 + - SFTP + - MINIO + - LOCAL + - COS + - KODO + - OneDrive + - WebDAV + type: string + required: + - fileDir + - fileName + - source + type: object + dto.Fail2BanBaseInfo: + properties: + banAction: + type: string + banTime: + type: string + findTime: + type: string + isActive: + type: boolean + isEnable: + type: boolean + isExist: + type: boolean + logPath: + type: string + maxRetry: + type: integer + port: + type: integer + version: + type: string + type: object + dto.Fail2BanSearch: + properties: + status: + enum: + - banned + - ignore + type: string + required: + - status + type: object + dto.Fail2BanUpdate: + properties: + key: + enum: + - port + - bantime + - findtime + - maxretry + - banaction + - logpath + - port + type: string + value: + type: string + required: + - key + type: object + dto.FirewallBaseInfo: + properties: + name: + type: string + pingStatus: + type: string + status: + type: string + version: + type: string + type: object + dto.FirewallOperation: + properties: + operation: + enum: + - start + - stop + - restart + - disablePing + - enablePing + type: string + required: + - operation + type: object + dto.ForBuckets: + properties: + accessKey: + type: string + credential: + type: string + type: + type: string + vars: + type: string + required: + - credential + - type + - vars + type: object + dto.ForwardRuleOperate: + properties: + rules: + items: + properties: + num: + type: string + operation: + enum: + - add + - remove + type: string + port: + type: string + protocol: + enum: + - tcp + - udp + - tcp/udp + type: string + targetIP: + type: string + targetPort: + type: string + required: + - operation + - port + - protocol + - targetPort + type: object + type: array + type: object + dto.FtpBaseInfo: + properties: + isActive: + type: boolean + isExist: + type: boolean + type: object + dto.FtpCreate: + properties: + description: + type: string + password: + type: string + path: + type: string + user: + type: string + required: + - password + - path + - user + type: object + dto.FtpLogSearch: + properties: + operation: + type: string + page: + type: integer + pageSize: + type: integer + user: + type: string + required: + - page + - pageSize + type: object + dto.FtpUpdate: + properties: + description: + type: string + id: + type: integer + password: + type: string + path: + type: string + status: + type: string + required: + - password + - path + type: object + dto.GPUInfo: + properties: + fanSpeed: + type: string + gpuUtil: + type: string + index: + type: integer + maxPowerLimit: + type: string + memTotal: + type: string + memUsed: + type: string + memoryUsage: + type: string + performanceState: + type: string + powerDraw: + type: string + powerUsage: + type: string + productName: + type: string + temperature: + type: string + type: object + dto.GenerateLoad: + properties: + encryptionMode: + enum: + - rsa + - ed25519 + - ecdsa + - dsa + type: string + required: + - encryptionMode + type: object + dto.GenerateSSH: + properties: + encryptionMode: + enum: + - rsa + - ed25519 + - ecdsa + - dsa + type: string + password: + type: string + required: + - encryptionMode + type: object + dto.GroupCreate: + properties: + id: + type: integer + name: + type: string + type: + type: string + required: + - name + - type + type: object + dto.GroupInfo: + properties: + id: + type: integer + isDefault: + type: boolean + name: + type: string + type: + type: string + type: object + dto.GroupSearch: + properties: + type: + type: string + required: + - type + type: object + dto.GroupUpdate: + properties: + id: + type: integer + isDefault: + type: boolean + name: + type: string + type: + type: string + required: + - type + type: object + dto.HostConnTest: + properties: + addr: + type: string + authMode: + enum: + - password + - key + type: string + passPhrase: + type: string + password: + type: string + port: + maximum: 65535 + minimum: 1 + type: integer + privateKey: + type: string + user: + type: string + required: + - addr + - port + - user + type: object + dto.HostHelper: + properties: + host: + type: string + ip: + type: string + type: object + dto.HostOperate: + properties: + addr: + type: string + authMode: + enum: + - password + - key + type: string + description: + type: string + groupID: + type: integer + id: + type: integer + name: + type: string + passPhrase: + type: string + password: + type: string + port: + maximum: 65535 + minimum: 1 + type: integer + privateKey: + type: string + rememberPassword: + type: boolean + user: + type: string + required: + - addr + - port + - user + type: object + dto.HostTree: + properties: + children: + items: + $ref: '#/definitions/dto.TreeChild' + type: array + id: + type: integer + label: + type: string + type: object + dto.ImageBuild: + properties: + dockerfile: + type: string + from: + type: string + name: + type: string + tags: + items: + type: string + type: array + required: + - dockerfile + - from + - name + type: object + dto.ImageInfo: + properties: + createdAt: + type: string + id: + type: string + isUsed: + type: boolean + size: + type: string + tags: + items: + type: string + type: array + type: object + dto.ImageLoad: + properties: + path: + type: string + required: + - path + type: object + dto.ImagePull: + properties: + imageName: + type: string + repoID: + type: integer + required: + - imageName + type: object + dto.ImagePush: + properties: + name: + type: string + repoID: + type: integer + tagName: + type: string + required: + - name + - repoID + - tagName + type: object + dto.ImageRepoDelete: + properties: + ids: + items: + type: integer + type: array + required: + - ids + type: object + dto.ImageRepoOption: + properties: + downloadUrl: + type: string + id: + type: integer + name: + type: string + type: object + dto.ImageRepoUpdate: + properties: + auth: + type: boolean + downloadUrl: + type: string + id: + type: integer + password: + maxLength: 256 + type: string + protocol: + type: string + username: + maxLength: 256 + type: string + type: object + dto.ImageSave: + properties: + name: + type: string + path: + type: string + tagName: + type: string + required: + - name + - path + - tagName + type: object + dto.ImageTag: + properties: + sourceID: + type: string + targetName: + type: string + required: + - sourceID + - targetName + type: object + dto.InspectReq: + properties: + id: + type: string + type: + type: string + required: + - id + - type + type: object + dto.LogOption: + properties: + logMaxFile: + type: string + logMaxSize: + type: string + type: object + dto.Login: + properties: + authMethod: + enum: + - jwt + - session + type: string + captcha: + type: string + captchaID: + type: string + ignoreCaptcha: + type: boolean + language: + enum: + - zh + - en + - tw + type: string + name: + type: string + password: + type: string + required: + - authMethod + - language + - name + - password + type: object + dto.MFALogin: + properties: + authMethod: + type: string + code: + type: string + name: + type: string + password: + type: string + required: + - code + - name + - password + type: object + dto.MfaCredential: + properties: + code: + type: string + interval: + type: string + secret: + type: string + required: + - code + - interval + - secret + type: object + dto.MonitorSearch: + properties: + endTime: + type: string + info: + type: string + param: + enum: + - all + - cpu + - memory + - load + - io + - network + type: string + startTime: + type: string + required: + - param + type: object + dto.MysqlDBCreate: + properties: + database: + type: string + description: + type: string + format: + enum: + - utf8mb4 + - utf8 + - gbk + - big5 + type: string + from: + enum: + - local + - remote + type: string + name: + type: string + password: + type: string + permission: + type: string + username: + type: string + required: + - database + - format + - from + - name + - password + - permission + - username + type: object + dto.MysqlDBDelete: + properties: + database: + type: string + deleteBackup: + type: boolean + forceDelete: + type: boolean + id: + type: integer + type: + enum: + - mysql + - mariadb + type: string + required: + - database + - id + - type + type: object + dto.MysqlDBDeleteCheck: + properties: + database: + type: string + id: + type: integer + type: + enum: + - mysql + - mariadb + type: string + required: + - database + - id + - type + type: object + dto.MysqlDBSearch: + properties: + database: + type: string + info: + type: string + order: + enum: + - "null" + - ascending + - descending + type: string + orderBy: + enum: + - name + - created_at + type: string + page: + type: integer + pageSize: + type: integer + required: + - database + - order + - orderBy + - page + - pageSize + type: object + dto.MysqlLoadDB: + properties: + database: + type: string + from: + enum: + - local + - remote + type: string + type: + enum: + - mysql + - mariadb + type: string + required: + - database + - from + - type + type: object + dto.MysqlOption: + properties: + database: + type: string + from: + type: string + id: + type: integer + name: + type: string + type: + type: string + type: object + dto.MysqlStatus: + properties: + Aborted_clients: + type: string + Aborted_connects: + type: string + Bytes_received: + type: string + Bytes_sent: + type: string + Com_commit: + type: string + Com_rollback: + type: string + Connections: + type: string + Created_tmp_disk_tables: + type: string + Created_tmp_tables: + type: string + File: + type: string + Innodb_buffer_pool_pages_dirty: + type: string + Innodb_buffer_pool_read_requests: + type: string + Innodb_buffer_pool_reads: + type: string + Key_read_requests: + type: string + Key_reads: + type: string + Key_write_requests: + type: string + Key_writes: + type: string + Max_used_connections: + type: string + Open_tables: + type: string + Opened_files: + type: string + Opened_tables: + type: string + Position: + type: string + Qcache_hits: + type: string + Qcache_inserts: + type: string + Questions: + type: string + Run: + type: string + Select_full_join: + type: string + Select_range_check: + type: string + Sort_merge_passes: + type: string + Table_locks_waited: + type: string + Threads_cached: + type: string + Threads_connected: + type: string + Threads_created: + type: string + Threads_running: + type: string + Uptime: + type: string + type: object + dto.MysqlVariables: + properties: + binlog_cache_size: + type: string + innodb_buffer_pool_size: + type: string + innodb_log_buffer_size: + type: string + join_buffer_size: + type: string + key_buffer_size: + type: string + long_query_time: + type: string + max_connections: + type: string + max_heap_table_size: + type: string + query_cache_size: + type: string + query_cache_type: + type: string + read_buffer_size: + type: string + read_rnd_buffer_size: + type: string + slow_query_log: + type: string + sort_buffer_size: + type: string + table_open_cache: + type: string + thread_cache_size: + type: string + thread_stack: + type: string + tmp_table_size: + type: string + type: object + dto.MysqlVariablesUpdate: + properties: + database: + type: string + type: + enum: + - mysql + - mariadb + type: string + variables: + items: + $ref: '#/definitions/dto.MysqlVariablesUpdateHelper' + type: array + required: + - database + - type + type: object + dto.MysqlVariablesUpdateHelper: + properties: + param: + type: string + value: {} + type: object + dto.NetworkCreate: + properties: + auxAddress: + items: + $ref: '#/definitions/dto.SettingUpdate' + type: array + auxAddressV6: + items: + $ref: '#/definitions/dto.SettingUpdate' + type: array + driver: + type: string + gateway: + type: string + gatewayV6: + type: string + ipRange: + type: string + ipRangeV6: + type: string + ipv4: + type: boolean + ipv6: + type: boolean + labels: + items: + type: string + type: array + name: + type: string + options: + items: + type: string + type: array + subnet: + type: string + subnetV6: + type: string + required: + - driver + - name + type: object + dto.NginxKey: + enum: + - index + - limit-conn + - ssl + - cache + - http-per + - proxy-cache + type: string + x-enum-varnames: + - Index + - LimitConn + - SSL + - CACHE + - HttpPer + - ProxyCache + dto.OneDriveInfo: + properties: + client_id: + type: string + client_secret: + type: string + redirect_uri: + type: string + type: object + dto.Operate: + properties: + operation: + type: string + required: + - operation + type: object + dto.OperateByID: + properties: + id: + type: integer + required: + - id + type: object + dto.OperationWithName: + properties: + name: + type: string + required: + - name + type: object + dto.OperationWithNameAndType: + properties: + name: + type: string + type: + type: string + required: + - type + type: object + dto.Options: + properties: + option: + type: string + type: object + dto.OsInfo: + properties: + diskSize: + type: integer + kernelArch: + type: string + kernelVersion: + type: string + os: + type: string + platform: + type: string + platformFamily: + type: string + type: object + dto.PageContainer: + properties: + excludeAppStore: + type: boolean + filters: + type: string + name: + type: string + order: + enum: + - "null" + - ascending + - descending + type: string + orderBy: + enum: + - name + - state + - created_at + type: string + page: + type: integer + pageSize: + type: integer + state: + enum: + - all + - created + - running + - paused + - restarting + - removing + - exited + - dead + type: string + required: + - order + - orderBy + - page + - pageSize + - state + type: object + dto.PageCronjob: + properties: + info: + type: string + order: + enum: + - "null" + - ascending + - descending + type: string + orderBy: + enum: + - name + - status + - created_at + type: string + page: + type: integer + pageSize: + type: integer + required: + - order + - orderBy + - page + - pageSize + type: object + dto.PageInfo: + properties: + page: + type: integer + pageSize: + type: integer + required: + - page + - pageSize + type: object + dto.PageResult: + properties: + items: {} + total: + type: integer + type: object + dto.PasswordUpdate: + properties: + newPassword: + type: string + oldPassword: + type: string + required: + - newPassword + - oldPassword + type: object + dto.PortHelper: + properties: + containerPort: + type: string + hostIP: + type: string + hostPort: + type: string + protocol: + type: string + type: object + dto.PortRuleOperate: + properties: + address: + type: string + description: + type: string + operation: + enum: + - add + - remove + type: string + port: + type: string + protocol: + enum: + - tcp + - udp + - tcp/udp + type: string + strategy: + enum: + - accept + - drop + type: string + required: + - operation + - port + - protocol + - strategy + type: object + dto.PortRuleUpdate: + properties: + newRule: + $ref: '#/definitions/dto.PortRuleOperate' + oldRule: + $ref: '#/definitions/dto.PortRuleOperate' + type: object + dto.PortUpdate: + properties: + serverPort: + maximum: 65535 + minimum: 1 + type: integer + required: + - serverPort + type: object + dto.PostgresqlBindUser: + properties: + database: + type: string + name: + type: string + password: + type: string + superUser: + type: boolean + username: + type: string + required: + - database + - name + - password + - username + type: object + dto.PostgresqlDBCreate: + properties: + database: + type: string + description: + type: string + format: + type: string + from: + enum: + - local + - remote + type: string + name: + type: string + password: + type: string + superUser: + type: boolean + username: + type: string + required: + - database + - from + - name + - password + - username + type: object + dto.PostgresqlDBDelete: + properties: + database: + type: string + deleteBackup: + type: boolean + forceDelete: + type: boolean + id: + type: integer + type: + enum: + - postgresql + type: string + required: + - database + - id + - type + type: object + dto.PostgresqlDBDeleteCheck: + properties: + database: + type: string + id: + type: integer + type: + enum: + - postgresql + type: string + required: + - database + - id + - type + type: object + dto.PostgresqlDBSearch: + properties: + database: + type: string + info: + type: string + order: + enum: + - "null" + - ascending + - descending + type: string + orderBy: + enum: + - name + - created_at + type: string + page: + type: integer + pageSize: + type: integer + required: + - database + - order + - orderBy + - page + - pageSize + type: object + dto.PostgresqlLoadDB: + properties: + database: + type: string + from: + enum: + - local + - remote + type: string + type: + enum: + - postgresql + type: string + required: + - database + - from + - type + type: object + dto.ProxyUpdate: + properties: + proxyPasswd: + type: string + proxyPasswdKeep: + type: string + proxyPort: + type: string + proxyType: + type: string + proxyUrl: + type: string + proxyUser: + type: string + type: object + dto.RecordSearch: + properties: + detailName: + type: string + name: + type: string + page: + type: integer + pageSize: + type: integer + type: + type: string + required: + - page + - pageSize + - type + type: object + dto.RecordSearchByCronjob: + properties: + cronjobID: + type: integer + page: + type: integer + pageSize: + type: integer + required: + - cronjobID + - page + - pageSize + type: object + dto.RedisCommand: + properties: + command: + type: string + id: + type: integer + name: + type: string + type: object + dto.RedisConf: + properties: + containerName: + type: string + database: + type: string + maxclients: + type: string + maxmemory: + type: string + name: + type: string + port: + type: integer + requirepass: + type: string + timeout: + type: string + required: + - database + type: object + dto.RedisConfPersistenceUpdate: + properties: + appendfsync: + type: string + appendonly: + type: string + database: + type: string + save: + type: string + type: + enum: + - aof + - rbd + type: string + required: + - database + - type + type: object + dto.RedisConfUpdate: + properties: + database: + type: string + maxclients: + type: string + maxmemory: + type: string + timeout: + type: string + required: + - database + type: object + dto.RedisPersistence: + properties: + appendfsync: + type: string + appendonly: + type: string + database: + type: string + save: + type: string + required: + - database + type: object + dto.RedisStatus: + properties: + connected_clients: + type: string + database: + type: string + instantaneous_ops_per_sec: + type: string + keyspace_hits: + type: string + keyspace_misses: + type: string + latest_fork_usec: + type: string + mem_fragmentation_ratio: + type: string + tcp_port: + type: string + total_commands_processed: + type: string + total_connections_received: + type: string + uptime_in_days: + type: string + used_memory: + type: string + used_memory_peak: + type: string + used_memory_rss: + type: string + required: + - database + type: object + dto.ResourceLimit: + properties: + cpu: + type: integer + memory: + type: integer + type: object + dto.RuleSearch: + properties: + info: + type: string + page: + type: integer + pageSize: + type: integer + status: + type: string + strategy: + type: string + type: + type: string + required: + - page + - pageSize + - type + type: object + dto.SSHConf: + properties: + file: + type: string + type: object + dto.SSHHistory: + properties: + address: + type: string + area: + type: string + authMode: + type: string + date: + type: string + dateStr: + type: string + message: + type: string + port: + type: string + status: + type: string + user: + type: string + type: object + dto.SSHInfo: + properties: + autoStart: + type: boolean + listenAddress: + type: string + message: + type: string + passwordAuthentication: + type: string + permitRootLogin: + type: string + port: + type: string + pubkeyAuthentication: + type: string + status: + type: string + useDNS: + type: string + type: object + dto.SSHLog: + properties: + failedCount: + type: integer + logs: + items: + $ref: '#/definitions/dto.SSHHistory' + type: array + successfulCount: + type: integer + totalCount: + type: integer + type: object + dto.SSHUpdate: + properties: + key: + type: string + newValue: + type: string + oldValue: + type: string + required: + - key + type: object + dto.SSLUpdate: + properties: + cert: + type: string + domain: + type: string + key: + type: string + ssl: + enum: + - enable + - disable + type: string + sslID: + type: integer + sslType: + enum: + - self + - select + - import + - import-paste + - import-local + type: string + required: + - ssl + - sslType + type: object + dto.SearchForTree: + properties: + info: + type: string + type: object + dto.SearchHostWithPage: + properties: + groupID: + type: integer + info: + type: string + page: + type: integer + pageSize: + type: integer + required: + - page + - pageSize + type: object + dto.SearchLgLogWithPage: + properties: + ip: + type: string + page: + type: integer + pageSize: + type: integer + status: + type: string + required: + - page + - pageSize + type: object + dto.SearchOpLogWithPage: + properties: + operation: + type: string + page: + type: integer + pageSize: + type: integer + source: + type: string + status: + type: string + required: + - page + - pageSize + type: object + dto.SearchRecord: + properties: + cronjobID: + type: integer + endTime: + type: string + page: + type: integer + pageSize: + type: integer + startTime: + type: string + status: + type: string + required: + - page + - pageSize + type: object + dto.SearchSSHLog: + properties: + Status: + enum: + - Success + - Failed + - All + type: string + info: + type: string + page: + type: integer + pageSize: + type: integer + required: + - Status + - page + - pageSize + type: object + dto.SearchWithPage: + properties: + info: + type: string + page: + type: integer + pageSize: + type: integer + required: + - page + - pageSize + type: object + dto.SettingInfo: + properties: + allowIPs: + type: string + appStoreLastModified: + type: string + appStoreSyncStatus: + type: string + appStoreVersion: + type: string + bindAddress: + type: string + bindDomain: + type: string + complexityVerification: + type: string + defaultNetwork: + type: string + developerMode: + type: string + dingVars: + type: string + dockerSockPath: + type: string + email: + type: string + emailVars: + type: string + expirationDays: + type: string + expirationTime: + type: string + fileRecycleBin: + type: string + ipv6: + type: string + language: + type: string + lastCleanData: + type: string + lastCleanSize: + type: string + lastCleanTime: + type: string + localTime: + type: string + menuTabs: + type: string + messageType: + type: string + mfaInterval: + type: string + mfaSecret: + type: string + mfaStatus: + type: string + monitorInterval: + type: string + monitorStatus: + type: string + monitorStoreDays: + type: string + noAuthSetting: + type: string + ntpSite: + type: string + panelName: + type: string + port: + type: string + proxyPasswd: + type: string + proxyPasswdKeep: + type: string + proxyPort: + type: string + proxyType: + type: string + proxyUrl: + type: string + proxyUser: + type: string + securityEntrance: + type: string + serverPort: + type: string + sessionTimeout: + type: string + snapshotIgnore: + type: string + ssl: + type: string + sslType: + type: string + systemIP: + type: string + systemVersion: + type: string + theme: + type: string + timeZone: + type: string + userName: + type: string + weChatVars: + type: string + xpackHideMenu: + type: string + type: object + dto.SettingUpdate: + properties: + key: + type: string + value: + type: string + required: + - key + type: object + dto.SnapshotBatchDelete: + properties: + deleteWithFile: + type: boolean + ids: + items: + type: integer + type: array + required: + - ids + type: object + dto.SnapshotCreate: + properties: + defaultDownload: + type: string + description: + maxLength: 256 + type: string + from: + type: string + id: + type: integer + secret: + type: string + required: + - defaultDownload + - from + type: object + dto.SnapshotImport: + properties: + description: + maxLength: 256 + type: string + from: + type: string + names: + items: + type: string + type: array + type: object + dto.SnapshotRecover: + properties: + id: + type: integer + isNew: + type: boolean + reDownload: + type: boolean + secret: + type: string + required: + - id + type: object + dto.SwapHelper: + properties: + isNew: + type: boolean + path: + type: string + size: + type: integer + used: + type: string + required: + - path + type: object + dto.TreeChild: + properties: + id: + type: integer + label: + type: string + type: object + dto.UpdateByFile: + properties: + file: + type: string + type: object + dto.UpdateByNameAndFile: + properties: + file: + type: string + name: + type: string + type: object + dto.UpdateDescription: + properties: + description: + maxLength: 256 + type: string + id: + type: integer + required: + - id + type: object + dto.UpdateFirewallDescription: + properties: + address: + type: string + description: + type: string + port: + type: string + protocol: + type: string + strategy: + enum: + - accept + - drop + type: string + type: + type: string + required: + - strategy + type: object + dto.Upgrade: + properties: + version: + type: string + required: + - version + type: object + dto.UpgradeInfo: + properties: + latestVersion: + type: string + newVersion: + type: string + releaseNote: + type: string + testVersion: + type: string + type: object + dto.UserLoginInfo: + properties: + mfaStatus: + type: string + name: + type: string + token: + type: string + type: object + dto.VolumeCreate: + properties: + driver: + type: string + labels: + items: + type: string + type: array + name: + type: string + options: + items: + type: string + type: array + required: + - driver + - name + type: object + dto.VolumeHelper: + properties: + containerDir: + type: string + mode: + type: string + sourceDir: + type: string + type: + type: string + type: object + files.FileInfo: + properties: + content: + type: string + extension: + type: string + favoriteID: + type: integer + gid: + type: string + group: + type: string + isDetail: + type: boolean + isDir: + type: boolean + isHidden: + type: boolean + isSymlink: + type: boolean + itemTotal: + type: integer + items: + items: + $ref: '#/definitions/files.FileInfo' + type: array + linkPath: + type: string + mimeType: + type: string + modTime: + type: string + mode: + type: string + name: + type: string + path: + type: string + size: + type: integer + type: + type: string + uid: + type: string + updateTime: + type: string + user: + type: string + type: object + mfa.Otp: + properties: + qrImage: + type: string + secret: + type: string + type: object + model.App: + properties: + createdAt: + type: string + crossVersionUpdate: + type: boolean + document: + type: string + github: + type: string + icon: + type: string + id: + type: integer + key: + type: string + lastModified: + type: integer + limit: + type: integer + name: + type: string + readMe: + type: string + recommend: + type: integer + required: + type: string + resource: + type: string + shortDescEn: + type: string + shortDescZh: + type: string + status: + type: string + tags: + items: + type: string + type: array + type: + type: string + updatedAt: + type: string + website: + type: string + type: object + model.AppInstall: + properties: + app: + $ref: '#/definitions/model.App' + appDetailId: + type: integer + appId: + type: integer + containerName: + type: string + createdAt: + type: string + description: + type: string + dockerCompose: + type: string + env: + type: string + httpPort: + type: integer + httpsPort: + type: integer + id: + type: integer + message: + type: string + name: + type: string + param: + type: string + serviceName: + type: string + status: + type: string + updatedAt: + type: string + version: + type: string + type: object + model.Tag: + properties: + createdAt: + type: string + id: + type: integer + key: + type: string + name: + type: string + sort: + type: integer + updatedAt: + type: string + type: object + model.Website: + properties: + IPV6: + type: boolean + accessLog: + type: boolean + alias: + type: string + appInstallId: + type: integer + createdAt: + type: string + defaultServer: + type: boolean + domains: + items: + $ref: '#/definitions/model.WebsiteDomain' + type: array + errorLog: + type: boolean + expireDate: + type: string + ftpId: + type: integer + group: + type: string + httpConfig: + type: string + id: + type: integer + primaryDomain: + type: string + protocol: + type: string + proxy: + type: string + proxyType: + type: string + remark: + type: string + rewrite: + type: string + runtimeID: + type: integer + siteDir: + type: string + status: + type: string + type: + type: string + updatedAt: + type: string + user: + type: string + webSiteGroupId: + type: integer + webSiteSSL: + $ref: '#/definitions/model.WebsiteSSL' + webSiteSSLId: + type: integer + type: object + model.WebsiteAcmeAccount: + properties: + createdAt: + type: string + eabHmacKey: + type: string + eabKid: + type: string + email: + type: string + id: + type: integer + keyType: + type: string + type: + type: string + updatedAt: + type: string + url: + type: string + type: object + model.WebsiteDnsAccount: + properties: + createdAt: + type: string + id: + type: integer + name: + type: string + type: + type: string + updatedAt: + type: string + type: object + model.WebsiteDomain: + properties: + createdAt: + type: string + domain: + type: string + id: + type: integer + port: + type: integer + updatedAt: + type: string + websiteId: + type: integer + type: object + model.WebsiteSSL: + properties: + acmeAccount: + $ref: '#/definitions/model.WebsiteAcmeAccount' + acmeAccountId: + type: integer + autoRenew: + type: boolean + caId: + type: integer + certURL: + type: string + createdAt: + type: string + description: + type: string + dir: + type: string + disableCNAME: + type: boolean + dnsAccount: + $ref: '#/definitions/model.WebsiteDnsAccount' + dnsAccountId: + type: integer + domains: + type: string + execShell: + type: boolean + expireDate: + type: string + id: + type: integer + keyType: + type: string + message: + type: string + nameserver1: + type: string + nameserver2: + type: string + organization: + type: string + pem: + type: string + primaryDomain: + type: string + privateKey: + type: string + provider: + type: string + pushDir: + type: boolean + shell: + type: string + skipDNS: + type: boolean + startDate: + type: string + status: + type: string + type: + type: string + updatedAt: + type: string + websites: + items: + $ref: '#/definitions/model.Website' + type: array + type: object + request.AppInstallCreate: + properties: + advanced: + type: boolean + allowPort: + type: boolean + appDetailId: + type: integer + containerName: + type: string + cpuQuota: + type: number + dockerCompose: + type: string + editCompose: + type: boolean + hostMode: + type: boolean + memoryLimit: + type: number + memoryUnit: + type: string + name: + type: string + params: + additionalProperties: true + type: object + pullImage: + type: boolean + services: + additionalProperties: + type: string + type: object + required: + - appDetailId + - name + type: object + request.AppInstalledIgnoreUpgrade: + properties: + detailID: + type: integer + operate: + enum: + - cancel + - ignore + type: string + required: + - detailID + - operate + type: object + request.AppInstalledInfo: + properties: + key: + type: string + name: + type: string + required: + - key + type: object + request.AppInstalledOperate: + properties: + backup: + type: boolean + backupId: + type: integer + deleteBackup: + type: boolean + deleteDB: + type: boolean + detailId: + type: integer + dockerCompose: + type: string + forceDelete: + type: boolean + installId: + type: integer + operate: + type: string + pullImage: + type: boolean + required: + - installId + - operate + type: object + request.AppInstalledSearch: + properties: + all: + type: boolean + name: + type: string + page: + type: integer + pageSize: + type: integer + sync: + type: boolean + tags: + items: + type: string + type: array + type: + type: string + unused: + type: boolean + update: + type: boolean + required: + - page + - pageSize + type: object + request.AppInstalledUpdate: + properties: + advanced: + type: boolean + allowPort: + type: boolean + containerName: + type: string + cpuQuota: + type: number + dockerCompose: + type: string + editCompose: + type: boolean + hostMode: + type: boolean + installId: + type: integer + memoryLimit: + type: number + memoryUnit: + type: string + params: + additionalProperties: true + type: object + pullImage: + type: boolean + required: + - installId + - params + type: object + request.AppSearch: + properties: + name: + type: string + page: + type: integer + pageSize: + type: integer + recommend: + type: boolean + resource: + type: string + tags: + items: + type: string + type: array + type: + type: string + required: + - page + - pageSize + type: object + request.DirSizeReq: + properties: + path: + type: string + required: + - path + type: object + request.ExposedPort: + properties: + containerPort: + type: integer + hostPort: + type: integer + type: object + request.FavoriteCreate: + properties: + path: + type: string + required: + - path + type: object + request.FavoriteDelete: + properties: + id: + type: integer + required: + - id + type: object + request.FileBatchDelete: + properties: + isDir: + type: boolean + paths: + items: + type: string + type: array + required: + - paths + type: object + request.FileCompress: + properties: + dst: + type: string + files: + items: + type: string + type: array + name: + type: string + replace: + type: boolean + secret: + type: string + type: + type: string + required: + - dst + - files + - name + - type + type: object + request.FileContentReq: + properties: + isDetail: + type: boolean + path: + type: string + required: + - path + type: object + request.FileCreate: + properties: + content: + type: string + isDir: + type: boolean + isLink: + type: boolean + isSymlink: + type: boolean + linkPath: + type: string + mode: + type: integer + path: + type: string + sub: + type: boolean + required: + - path + type: object + request.FileDeCompress: + properties: + dst: + type: string + path: + type: string + secret: + type: string + type: + type: string + required: + - dst + - path + - type + type: object + request.FileDelete: + properties: + forceDelete: + type: boolean + isDir: + type: boolean + path: + type: string + required: + - path + type: object + request.FileDownload: + properties: + compress: + type: boolean + name: + type: string + paths: + items: + type: string + type: array + type: + type: string + required: + - name + - paths + - type + type: object + request.FileEdit: + properties: + content: + type: string + path: + type: string + required: + - path + type: object + request.FileMove: + properties: + cover: + type: boolean + name: + type: string + newPath: + type: string + oldPaths: + items: + type: string + type: array + type: + type: string + required: + - newPath + - oldPaths + - type + type: object + request.FileOption: + properties: + containSub: + type: boolean + dir: + type: boolean + expand: + type: boolean + isDetail: + type: boolean + page: + type: integer + pageSize: + type: integer + path: + type: string + search: + type: string + showHidden: + type: boolean + sortBy: + type: string + sortOrder: + type: string + type: object + request.FilePathCheck: + properties: + path: + type: string + required: + - path + type: object + request.FileReadByLineReq: + properties: + ID: + type: integer + latest: + type: boolean + name: + type: string + page: + type: integer + pageSize: + type: integer + type: + type: string + required: + - page + - pageSize + - type + type: object + request.FileRename: + properties: + newName: + type: string + oldName: + type: string + required: + - newName + - oldName + type: object + request.FileRoleReq: + properties: + group: + type: string + mode: + type: integer + paths: + items: + type: string + type: array + sub: + type: boolean + user: + type: string + required: + - group + - mode + - paths + - user + type: object + request.FileRoleUpdate: + properties: + group: + type: string + path: + type: string + sub: + type: boolean + user: + type: string + required: + - group + - path + - user + type: object + request.FileWget: + properties: + ignoreCertificate: + type: boolean + name: + type: string + path: + type: string + url: + type: string + required: + - name + - path + - url + type: object + request.HostToolConfig: + properties: + content: + type: string + operate: + enum: + - get + - set + type: string + type: + enum: + - supervisord + type: string + required: + - type + type: object + request.HostToolCreate: + properties: + configPath: + type: string + serviceName: + type: string + type: + type: string + required: + - type + type: object + request.HostToolLogReq: + properties: + type: + enum: + - supervisord + type: string + required: + - type + type: object + request.HostToolReq: + properties: + operate: + enum: + - status + - restart + - start + - stop + type: string + type: + enum: + - supervisord + type: string + required: + - type + type: object + request.NewAppInstall: + properties: + advanced: + type: boolean + allowPort: + type: boolean + appDetailID: + type: integer + containerName: + type: string + cpuQuota: + type: number + dockerCompose: + type: string + editCompose: + type: boolean + hostMode: + type: boolean + memoryLimit: + type: number + memoryUnit: + type: string + name: + type: string + params: + additionalProperties: true + type: object + pullImage: + type: boolean + type: object + request.NginxAntiLeechUpdate: + properties: + blocked: + type: boolean + cache: + type: boolean + cacheTime: + type: integer + cacheUint: + type: string + enable: + type: boolean + extends: + type: string + logEnable: + type: boolean + noneRef: + type: boolean + return: + type: string + serverNames: + items: + type: string + type: array + websiteID: + type: integer + required: + - extends + - return + - websiteID + type: object + request.NginxAuthReq: + properties: + websiteID: + type: integer + required: + - websiteID + type: object + request.NginxAuthUpdate: + properties: + operate: + type: string + password: + type: string + remark: + type: string + username: + type: string + websiteID: + type: integer + required: + - operate + - websiteID + type: object + request.NginxCommonReq: + properties: + websiteID: + type: integer + required: + - websiteID + type: object + request.NginxConfigFileUpdate: + properties: + backup: + type: boolean + content: + type: string + required: + - content + type: object + request.NginxConfigUpdate: + properties: + operate: + enum: + - add + - update + - delete + type: string + params: {} + scope: + $ref: '#/definitions/dto.NginxKey' + websiteId: + type: integer + required: + - operate + type: object + request.NginxProxyUpdate: + properties: + content: + type: string + name: + type: string + websiteID: + type: integer + required: + - content + - name + - websiteID + type: object + request.NginxRedirectReq: + properties: + domains: + items: + type: string + type: array + enable: + type: boolean + keepPath: + type: boolean + name: + type: string + operate: + type: string + path: + type: string + redirect: + type: string + redirectRoot: + type: boolean + target: + type: string + type: + type: string + websiteID: + type: integer + required: + - name + - operate + - redirect + - target + - type + - websiteID + type: object + request.NginxRedirectUpdate: + properties: + content: + type: string + name: + type: string + websiteID: + type: integer + required: + - content + - name + - websiteID + type: object + request.NginxRewriteReq: + properties: + name: + type: string + websiteId: + type: integer + required: + - name + - websiteId + type: object + request.NginxRewriteUpdate: + properties: + content: + type: string + name: + type: string + websiteId: + type: integer + required: + - name + - websiteId + type: object + request.NginxScopeReq: + properties: + scope: + $ref: '#/definitions/dto.NginxKey' + websiteId: + type: integer + required: + - scope + type: object + request.NodeModuleReq: + properties: + ID: + type: integer + required: + - ID + type: object + request.NodePackageReq: + properties: + codeDir: + type: string + type: object + request.PHPExtensionsCreate: + properties: + extensions: + type: string + name: + type: string + required: + - extensions + - name + type: object + request.PHPExtensionsDelete: + properties: + id: + type: integer + required: + - id + type: object + request.PHPExtensionsSearch: + properties: + all: + type: boolean + page: + type: integer + pageSize: + type: integer + required: + - page + - pageSize + type: object + request.PHPExtensionsUpdate: + properties: + extensions: + type: string + id: + type: integer + required: + - extensions + - id + type: object + request.PortUpdate: + properties: + key: + type: string + name: + type: string + port: + type: integer + type: object + request.ProcessReq: + properties: + PID: + type: integer + required: + - PID + type: object + request.RecycleBinReduce: + properties: + from: + type: string + name: + type: string + rName: + type: string + required: + - from + - rName + type: object + request.RuntimeCreate: + properties: + appDetailId: + type: integer + clean: + type: boolean + codeDir: + type: string + exposedPorts: + items: + $ref: '#/definitions/request.ExposedPort' + type: array + image: + type: string + install: + type: boolean + name: + type: string + params: + additionalProperties: true + type: object + port: + type: integer + resource: + type: string + source: + type: string + type: + type: string + version: + type: string + type: object + request.RuntimeDelete: + properties: + forceDelete: + type: boolean + id: + type: integer + type: object + request.RuntimeOperate: + properties: + ID: + type: integer + operate: + type: string + type: object + request.RuntimeSearch: + properties: + name: + type: string + page: + type: integer + pageSize: + type: integer + status: + type: string + type: + type: string + required: + - page + - pageSize + type: object + request.RuntimeUpdate: + properties: + clean: + type: boolean + codeDir: + type: string + exposedPorts: + items: + $ref: '#/definitions/request.ExposedPort' + type: array + id: + type: integer + image: + type: string + install: + type: boolean + name: + type: string + params: + additionalProperties: true + type: object + port: + type: integer + rebuild: + type: boolean + source: + type: string + version: + type: string + type: object + request.SearchUploadWithPage: + properties: + page: + type: integer + pageSize: + type: integer + path: + type: string + required: + - page + - pageSize + - path + type: object + request.SupervisorProcessConfig: + properties: + command: + type: string + dir: + type: string + name: + type: string + numprocs: + type: string + operate: + type: string + user: + type: string + type: object + request.SupervisorProcessFileReq: + properties: + content: + type: string + file: + enum: + - out.log + - err.log + - config + type: string + name: + type: string + operate: + enum: + - get + - clear + - update + type: string + required: + - file + - name + - operate + type: object + request.WebsiteAcmeAccountCreate: + properties: + eabHmacKey: + type: string + eabKid: + type: string + email: + type: string + keyType: + enum: + - P256 + - P384 + - "2048" + - "3072" + - "4096" + - "8192" + type: string + type: + enum: + - letsencrypt + - zerossl + - buypass + - google + type: string + required: + - email + - keyType + - type + type: object + request.WebsiteBatchDelReq: + properties: + ids: + items: + type: integer + type: array + required: + - ids + type: object + request.WebsiteCACreate: + properties: + city: + type: string + commonName: + type: string + country: + type: string + keyType: + enum: + - P256 + - P384 + - "2048" + - "3072" + - "4096" + - "8192" + type: string + name: + type: string + organization: + type: string + organizationUint: + type: string + province: + type: string + required: + - commonName + - country + - keyType + - name + - organization + type: object + request.WebsiteCAObtain: + properties: + autoRenew: + type: boolean + description: + type: string + dir: + type: string + domains: + type: string + execShell: + type: boolean + id: + type: integer + keyType: + enum: + - P256 + - P384 + - "2048" + - "3072" + - "4096" + - "8192" + type: string + pushDir: + type: boolean + renew: + type: boolean + shell: + type: string + sslID: + type: integer + time: + type: integer + unit: + type: string + required: + - domains + - id + - keyType + - time + - unit + type: object + request.WebsiteCASearch: + properties: + page: + type: integer + pageSize: + type: integer + required: + - page + - pageSize + type: object + request.WebsiteCommonReq: + properties: + id: + type: integer + required: + - id + type: object + request.WebsiteCreate: + properties: + IPV6: + type: boolean + alias: + type: string + appID: + type: integer + appInstall: + $ref: '#/definitions/request.NewAppInstall' + appInstallID: + type: integer + appType: + enum: + - new + - installed + type: string + ftpPassword: + type: string + ftpUser: + type: string + otherDomains: + type: string + port: + type: integer + primaryDomain: + type: string + proxy: + type: string + proxyType: + type: string + remark: + type: string + runtimeID: + type: integer + type: + type: string + webSiteGroupID: + type: integer + required: + - alias + - primaryDomain + - type + - webSiteGroupID + type: object + request.WebsiteDNSReq: + properties: + acmeAccountId: + type: integer + domains: + items: + type: string + type: array + required: + - acmeAccountId + - domains + type: object + request.WebsiteDefaultUpdate: + properties: + id: + type: integer + type: object + request.WebsiteDelete: + properties: + deleteApp: + type: boolean + deleteBackup: + type: boolean + forceDelete: + type: boolean + id: + type: integer + required: + - id + type: object + request.WebsiteDnsAccountCreate: + properties: + authorization: + additionalProperties: + type: string + type: object + name: + type: string + type: + type: string + required: + - authorization + - name + - type + type: object + request.WebsiteDnsAccountUpdate: + properties: + authorization: + additionalProperties: + type: string + type: object + id: + type: integer + name: + type: string + type: + type: string + required: + - authorization + - id + - name + - type + type: object + request.WebsiteDomainCreate: + properties: + domains: + type: string + websiteID: + type: integer + required: + - domains + - websiteID + type: object + request.WebsiteDomainDelete: + properties: + id: + type: integer + required: + - id + type: object + request.WebsiteHTTPSOp: + properties: + SSLProtocol: + items: + type: string + type: array + algorithm: + type: string + certificate: + type: string + certificatePath: + type: string + enable: + type: boolean + hsts: + type: boolean + httpConfig: + enum: + - HTTPSOnly + - HTTPAlso + - HTTPToHTTPS + type: string + importType: + type: string + privateKey: + type: string + privateKeyPath: + type: string + type: + enum: + - existed + - auto + - manual + type: string + websiteId: + type: integer + websiteSSLId: + type: integer + required: + - websiteId + type: object + request.WebsiteHtmlUpdate: + properties: + content: + type: string + type: + type: string + required: + - content + - type + type: object + request.WebsiteInstallCheckReq: + properties: + InstallIds: + items: + type: integer + type: array + type: object + request.WebsiteLogReq: + properties: + id: + type: integer + logType: + type: string + operate: + type: string + page: + type: integer + pageSize: + type: integer + required: + - id + - logType + - operate + type: object + request.WebsiteNginxUpdate: + properties: + content: + type: string + id: + type: integer + required: + - content + - id + type: object + request.WebsiteOp: + properties: + id: + type: integer + operate: + type: string + required: + - id + type: object + request.WebsitePHPConfigUpdate: + properties: + disableFunctions: + items: + type: string + type: array + id: + type: integer + params: + additionalProperties: + type: string + type: object + scope: + type: string + uploadMaxSize: + type: string + required: + - id + - scope + type: object + request.WebsitePHPFileUpdate: + properties: + content: + type: string + id: + type: integer + type: + type: string + required: + - content + - id + - type + type: object + request.WebsitePHPVersionReq: + properties: + retainConfig: + type: boolean + runtimeID: + type: integer + websiteID: + type: integer + required: + - runtimeID + - websiteID + type: object + request.WebsiteProxyConfig: + properties: + cache: + type: boolean + cacheTime: + type: integer + cacheUnit: + type: string + content: + type: string + enable: + type: boolean + filePath: + type: string + id: + type: integer + match: + type: string + modifier: + type: string + name: + type: string + operate: + type: string + proxyHost: + type: string + proxyPass: + type: string + replaces: + additionalProperties: + type: string + type: object + sni: + type: boolean + required: + - id + - match + - name + - operate + - proxyHost + - proxyPass + type: object + request.WebsiteProxyReq: + properties: + id: + type: integer + required: + - id + type: object + request.WebsiteResourceReq: + properties: + id: + type: integer + required: + - id + type: object + request.WebsiteSSLApply: + properties: + ID: + type: integer + nameservers: + items: + type: string + type: array + skipDNSCheck: + type: boolean + required: + - ID + type: object + request.WebsiteSSLCreate: + properties: + acmeAccountId: + type: integer + apply: + type: boolean + autoRenew: + type: boolean + description: + type: string + dir: + type: string + disableCNAME: + type: boolean + dnsAccountId: + type: integer + execShell: + type: boolean + id: + type: integer + keyType: + type: string + nameserver1: + type: string + nameserver2: + type: string + otherDomains: + type: string + primaryDomain: + type: string + provider: + type: string + pushDir: + type: boolean + shell: + type: string + skipDNS: + type: boolean + required: + - acmeAccountId + - primaryDomain + - provider + type: object + request.WebsiteSSLSearch: + properties: + acmeAccountID: + type: string + page: + type: integer + pageSize: + type: integer + required: + - page + - pageSize + type: object + request.WebsiteSSLUpdate: + properties: + acmeAccountId: + type: integer + apply: + type: boolean + autoRenew: + type: boolean + description: + type: string + dir: + type: string + disableCNAME: + type: boolean + dnsAccountId: + type: integer + execShell: + type: boolean + id: + type: integer + keyType: + type: string + nameserver1: + type: string + nameserver2: + type: string + otherDomains: + type: string + primaryDomain: + type: string + provider: + type: string + pushDir: + type: boolean + shell: + type: string + skipDNS: + type: boolean + required: + - id + - primaryDomain + - provider + type: object + request.WebsiteSSLUpload: + properties: + certificate: + type: string + certificatePath: + type: string + description: + type: string + privateKey: + type: string + privateKeyPath: + type: string + sslID: + type: integer + type: + enum: + - paste + - local + type: string + required: + - type + type: object + request.WebsiteSearch: + properties: + name: + type: string + order: + enum: + - "null" + - ascending + - descending + type: string + orderBy: + enum: + - primary_domain + - type + - status + - created_at + - expire_date + type: string + page: + type: integer + pageSize: + type: integer + websiteGroupId: + type: integer + required: + - order + - orderBy + - page + - pageSize + type: object + request.WebsiteUpdate: + properties: + IPV6: + type: boolean + expireDate: + type: string + id: + type: integer + primaryDomain: + type: string + remark: + type: string + webSiteGroupID: + type: integer + required: + - id + - primaryDomain + type: object + request.WebsiteUpdateDir: + properties: + id: + type: integer + siteDir: + type: string + required: + - id + - siteDir + type: object + request.WebsiteUpdateDirPermission: + properties: + group: + type: string + id: + type: integer + user: + type: string + required: + - group + - id + - user + type: object + response.AppDTO: + properties: + createdAt: + type: string + crossVersionUpdate: + type: boolean + document: + type: string + github: + type: string + icon: + type: string + id: + type: integer + installed: + type: boolean + key: + type: string + lastModified: + type: integer + limit: + type: integer + name: + type: string + readMe: + type: string + recommend: + type: integer + required: + type: string + resource: + type: string + shortDescEn: + type: string + shortDescZh: + type: string + status: + type: string + tags: + items: + $ref: '#/definitions/model.Tag' + type: array + type: + type: string + updatedAt: + type: string + versions: + items: + type: string + type: array + website: + type: string + type: object + response.AppDetailDTO: + properties: + appId: + type: integer + createdAt: + type: string + dockerCompose: + type: string + downloadCallBackUrl: + type: string + downloadUrl: + type: string + enable: + type: boolean + hostMode: + type: boolean + id: + type: integer + ignoreUpgrade: + type: boolean + image: + type: string + lastModified: + type: integer + lastVersion: + type: string + params: {} + status: + type: string + update: + type: boolean + updatedAt: + type: string + version: + type: string + type: object + response.AppInstalledCheck: + properties: + app: + type: string + appInstallId: + type: integer + containerName: + type: string + createdAt: + type: string + httpPort: + type: integer + httpsPort: + type: integer + installPath: + type: string + isExist: + type: boolean + lastBackupAt: + type: string + name: + type: string + status: + type: string + version: + type: string + type: object + response.AppParam: + properties: + edit: + type: boolean + key: + type: string + labelEn: + type: string + labelZh: + type: string + multiple: + type: boolean + required: + type: boolean + rule: + type: string + showValue: + type: string + type: + type: string + value: {} + values: {} + type: object + response.AppService: + properties: + config: {} + from: + type: string + label: + type: string + value: + type: string + type: object + response.FileInfo: + properties: + content: + type: string + extension: + type: string + favoriteID: + type: integer + gid: + type: string + group: + type: string + isDetail: + type: boolean + isDir: + type: boolean + isHidden: + type: boolean + isSymlink: + type: boolean + itemTotal: + type: integer + items: + items: + $ref: '#/definitions/files.FileInfo' + type: array + linkPath: + type: string + mimeType: + type: string + modTime: + type: string + mode: + type: string + name: + type: string + path: + type: string + size: + type: integer + type: + type: string + uid: + type: string + updateTime: + type: string + user: + type: string + type: object + response.FileTree: + properties: + children: + items: + $ref: '#/definitions/response.FileTree' + type: array + extension: + type: string + id: + type: string + isDir: + type: boolean + name: + type: string + path: + type: string + type: object + response.IgnoredApp: + properties: + detailID: + type: integer + icon: + type: string + name: + type: string + version: + type: string + type: object + response.NginxParam: + properties: + name: + type: string + params: + items: + type: string + type: array + type: object + response.NginxStatus: + properties: + accepts: + type: string + active: + type: string + handled: + type: string + reading: + type: string + requests: + type: string + waiting: + type: string + writing: + type: string + type: object + response.PHPConfig: + properties: + disableFunctions: + items: + type: string + type: array + params: + additionalProperties: + type: string + type: object + uploadMaxSize: + type: string + type: object + response.PHPExtensionsDTO: + properties: + createdAt: + type: string + extensions: + type: string + id: + type: integer + name: + type: string + updatedAt: + type: string + type: object + response.WebsiteAcmeAccountDTO: + properties: + createdAt: + type: string + eabHmacKey: + type: string + eabKid: + type: string + email: + type: string + id: + type: integer + keyType: + type: string + type: + type: string + updatedAt: + type: string + url: + type: string + type: object + response.WebsiteCADTO: + properties: + city: + type: string + commonName: + type: string + country: + type: string + createdAt: + type: string + csr: + type: string + id: + type: integer + keyType: + type: string + name: + type: string + organization: + type: string + organizationUint: + type: string + privateKey: + type: string + province: + type: string + updatedAt: + type: string + type: object + response.WebsiteDNSRes: + properties: + domain: + type: string + err: + type: string + resolve: + type: string + value: + type: string + type: object + response.WebsiteDTO: + properties: + IPV6: + type: boolean + accessLog: + type: boolean + accessLogPath: + type: string + alias: + type: string + appInstallId: + type: integer + appName: + type: string + createdAt: + type: string + defaultServer: + type: boolean + domains: + items: + $ref: '#/definitions/model.WebsiteDomain' + type: array + errorLog: + type: boolean + errorLogPath: + type: string + expireDate: + type: string + ftpId: + type: integer + group: + type: string + httpConfig: + type: string + id: + type: integer + primaryDomain: + type: string + protocol: + type: string + proxy: + type: string + proxyType: + type: string + remark: + type: string + rewrite: + type: string + runtimeID: + type: integer + runtimeName: + type: string + siteDir: + type: string + sitePath: + type: string + status: + type: string + type: + type: string + updatedAt: + type: string + user: + type: string + webSiteGroupId: + type: integer + webSiteSSL: + $ref: '#/definitions/model.WebsiteSSL' + webSiteSSLId: + type: integer + type: object + response.WebsiteHTTPS: + properties: + SSL: + $ref: '#/definitions/model.WebsiteSSL' + SSLProtocol: + items: + type: string + type: array + algorithm: + type: string + enable: + type: boolean + hsts: + type: boolean + httpConfig: + type: string + type: object + response.WebsiteLog: + properties: + content: + type: string + enable: + type: boolean + end: + type: boolean + path: + type: string + type: object + response.WebsiteNginxConfig: + properties: + enable: + type: boolean + params: + items: + $ref: '#/definitions/response.NginxParam' + type: array + type: object + response.WebsitePreInstallCheck: + properties: + appName: + type: string + name: + type: string + status: + type: string + version: + type: string + type: object +host: localhost +info: + contact: {} + description: 开源Linux面板 + license: + name: Apache 2.0 + url: http://www.apache.org/licenses/LICENSE-2.0.html + termsOfService: http://swagger.io/terms/ + title: 1Panel + version: "1.0" +paths: + /apps/:key: + get: + consumes: + - application/json + description: 通过 key 获取应用信息 + parameters: + - description: app key + in: path + name: key + required: true + type: string + responses: + "200": + description: OK + schema: + $ref: '#/definitions/response.AppDTO' + security: + - ApiKeyAuth: [] + summary: Search app by key + tags: + - App + /apps/checkupdate: + get: + description: 获取应用更新版本 + responses: + "200": + description: OK + security: + - ApiKeyAuth: [] + summary: Get app list update + tags: + - App + /apps/detail/:appId/:version/:type: + get: + consumes: + - application/json + description: 通过 appid 获取应用详情 + parameters: + - description: app id + in: path + name: appId + required: true + type: integer + - description: app 版本 + in: path + name: version + required: true + type: string + - description: app 类型 + in: path + name: version + required: true + type: string + responses: + "200": + description: OK + schema: + $ref: '#/definitions/response.AppDetailDTO' + security: + - ApiKeyAuth: [] + summary: Search app detail by appid + tags: + - App + /apps/details/:id: + get: + consumes: + - application/json + description: 通过 id 获取应用详情 + parameters: + - description: id + in: path + name: appId + required: true + type: integer + responses: + "200": + description: OK + schema: + $ref: '#/definitions/response.AppDetailDTO' + security: + - ApiKeyAuth: [] + summary: Get app detail by id + tags: + - App + /apps/ignored: + get: + consumes: + - application/json + description: 获取忽略的应用版本 + responses: + "200": + description: OK + schema: + $ref: '#/definitions/response.IgnoredApp' + security: + - ApiKeyAuth: [] + summary: Get Ignore App + tags: + - App + /apps/install: + post: + consumes: + - application/json + description: 安装应用 + parameters: + - description: request + in: body + name: request + required: true + schema: + $ref: '#/definitions/request.AppInstallCreate' + responses: + "200": + description: OK + schema: + $ref: '#/definitions/model.AppInstall' + security: + - ApiKeyAuth: [] + summary: Install app + tags: + - App + x-panel-log: + BeforeFunctions: + - db: app_installs + input_column: name + input_value: name + isList: false + output_column: app_id + output_value: appId + - db: apps + info: appId + isList: false + output_column: key + output_value: appKey + bodyKeys: + - name + formatEN: Install app [appKey]-[name] + formatZH: 安装应用 [appKey]-[name] + paramKeys: [] + /apps/installed/check: + post: + consumes: + - application/json + description: 检查应用安装情况 + parameters: + - description: request + in: body + name: request + required: true + schema: + $ref: '#/definitions/request.AppInstalledInfo' + responses: + "200": + description: OK + schema: + $ref: '#/definitions/response.AppInstalledCheck' + security: + - ApiKeyAuth: [] + summary: Check app installed + tags: + - App + /apps/installed/conf: + post: + consumes: + - application/json + description: 通过 key 获取应用默认配置 + parameters: + - description: request + in: body + name: request + required: true + schema: + $ref: '#/definitions/dto.OperationWithNameAndType' + responses: + "200": + description: OK + schema: + type: string + security: + - ApiKeyAuth: [] + summary: Search default config by key + tags: + - App + /apps/installed/conninfo/:key: + get: + consumes: + - application/json + description: 获取应用连接信息 + parameters: + - description: request + in: body + name: request + required: true + schema: + $ref: '#/definitions/dto.OperationWithNameAndType' + responses: + "200": + description: OK + schema: + type: string + security: + - ApiKeyAuth: [] + summary: Search app password by key + tags: + - App + /apps/installed/delete/check/:appInstallId: + get: + consumes: + - application/json + description: 删除前检查 + parameters: + - description: App install id + in: path + name: appInstallId + required: true + type: integer + responses: + "200": + description: OK + schema: + items: + $ref: '#/definitions/dto.AppResource' + type: array + security: + - ApiKeyAuth: [] + summary: Check before delete + tags: + - App + /apps/installed/ignore: + post: + consumes: + - application/json + description: 忽略应用升级版本 + parameters: + - description: request + in: body + name: request + required: true + schema: + $ref: '#/definitions/request.AppInstalledIgnoreUpgrade' + responses: + "200": + description: OK + security: + - ApiKeyAuth: [] + summary: ignore App Update + tags: + - App + x-panel-log: + BeforeFunctions: [] + bodyKeys: + - installId + formatEN: Application param update [installId] + formatZH: 忽略应用 [installId] 版本升级 + paramKeys: [] + /apps/installed/list: + get: + consumes: + - application/json + description: 获取已安装应用列表 + responses: + "200": + description: OK + schema: + items: + $ref: '#/definitions/dto.AppInstallInfo' + type: array + security: + - ApiKeyAuth: [] + summary: List app installed + tags: + - App + /apps/installed/loadport: + post: + consumes: + - application/json + description: 获取应用端口 + parameters: + - description: request + in: body + name: request + required: true + schema: + $ref: '#/definitions/dto.OperationWithNameAndType' + responses: + "200": + description: OK + schema: + type: integer + security: + - ApiKeyAuth: [] + summary: Search app port by key + tags: + - App + /apps/installed/op: + post: + consumes: + - application/json + description: 操作已安装应用 + parameters: + - description: request + in: body + name: request + required: true + schema: + $ref: '#/definitions/request.AppInstalledOperate' + responses: + "200": + description: OK + security: + - ApiKeyAuth: [] + summary: Operate installed app + tags: + - App + x-panel-log: + BeforeFunctions: + - db: app_installs + input_column: id + input_value: installId + isList: false + output_column: app_id + output_value: appId + - db: app_installs + input_column: id + input_value: installId + isList: false + output_column: name + output_value: appName + - db: apps + input_column: id + input_value: appId + isList: false + output_column: key + output_value: appKey + bodyKeys: + - installId + - operate + formatEN: '[operate] App [appKey][appName]' + formatZH: '[operate] 应用 [appKey][appName]' + paramKeys: [] + /apps/installed/params/:appInstallId: + get: + consumes: + - application/json + description: 通过 install id 获取应用参数 + parameters: + - description: request + in: path + name: appInstallId + required: true + type: string + responses: + "200": + description: OK + schema: + $ref: '#/definitions/response.AppParam' + security: + - ApiKeyAuth: [] + summary: Search params by appInstallId + tags: + - App + /apps/installed/params/update: + post: + consumes: + - application/json + description: 修改应用参数 + parameters: + - description: request + in: body + name: request + required: true + schema: + $ref: '#/definitions/request.AppInstalledUpdate' + responses: + "200": + description: OK + security: + - ApiKeyAuth: [] + summary: Change app params + tags: + - App + x-panel-log: + BeforeFunctions: [] + bodyKeys: + - installId + formatEN: Application param update [installId] + formatZH: 应用参数修改 [installId] + paramKeys: [] + /apps/installed/port/change: + post: + consumes: + - application/json + description: 修改应用端口 + parameters: + - description: request + in: body + name: request + required: true + schema: + $ref: '#/definitions/request.PortUpdate' + responses: + "200": + description: OK + security: + - ApiKeyAuth: [] + summary: Change app port + tags: + - App + x-panel-log: + BeforeFunctions: [] + bodyKeys: + - key + - name + - port + formatEN: Application port update [key]-[name] => [port] + formatZH: 应用端口修改 [key]-[name] => [port] + paramKeys: [] + /apps/installed/search: + post: + consumes: + - application/json + description: 分页获取已安装应用列表 + parameters: + - description: request + in: body + name: request + required: true + schema: + $ref: '#/definitions/request.AppInstalledSearch' + responses: + "200": + description: OK + security: + - ApiKeyAuth: [] + summary: Page app installed + tags: + - App + /apps/installed/sync: + post: + description: 同步已安装应用列表 + responses: + "200": + description: OK + security: + - ApiKeyAuth: [] + summary: Sync app installed + tags: + - App + x-panel-log: + BeforeFunctions: [] + bodyKeys: [] + formatEN: Sync the list of installed apps + formatZH: 同步已安装应用列表 + paramKeys: [] + /apps/installed/update/versions: + post: + consumes: + - application/json + description: 通过 install id 获取应用更新版本 + parameters: + - description: request + in: path + name: appInstallId + required: true + type: integer + responses: + "200": + description: OK + schema: + items: + $ref: '#/definitions/dto.AppVersion' + type: array + security: + - ApiKeyAuth: [] + summary: Search app update version by install id + tags: + - App + /apps/search: + post: + consumes: + - application/json + description: 获取应用列表 + parameters: + - description: request + in: body + name: request + required: true + schema: + $ref: '#/definitions/request.AppSearch' + responses: + "200": + description: OK + security: + - ApiKeyAuth: [] + summary: List apps + tags: + - App + /apps/services/:key: + get: + consumes: + - application/json + description: 通过 key 获取应用 service + parameters: + - description: request + in: path + name: key + required: true + type: string + responses: + "200": + description: OK + schema: + items: + $ref: '#/definitions/response.AppService' + type: array + security: + - ApiKeyAuth: [] + summary: Search app service by key + tags: + - App + /apps/sync: + post: + description: 同步应用列表 + responses: + "200": + description: OK + security: + - ApiKeyAuth: [] + summary: Sync app list + tags: + - App + x-panel-log: + BeforeFunctions: [] + bodyKeys: [] + formatEN: App store synchronization + formatZH: 应用商店同步 + paramKeys: [] + /auth/captcha: + get: + description: 加载验证码 + responses: + "200": + description: OK + schema: + $ref: '#/definitions/dto.CaptchaResponse' + summary: Load captcha + tags: + - Auth + /auth/demo: + get: + description: 判断是否为demo环境 + responses: + "200": + description: OK + summary: Check System isDemo + tags: + - Auth + /auth/issafety: + get: + description: 获取系统安全登录状态 + responses: + "200": + description: OK + summary: Load safety status + tags: + - Auth + /auth/language: + get: + description: 获取系统语言设置 + responses: + "200": + description: OK + summary: Load System Language + tags: + - Auth + /auth/login: + post: + consumes: + - application/json + description: 用户登录 + parameters: + - description: 安全入口 base64 加密串 + in: header + name: EntranceCode + required: true + type: string + - description: request + in: body + name: request + required: true + schema: + $ref: '#/definitions/dto.Login' + responses: + "200": + description: OK + schema: + $ref: '#/definitions/dto.UserLoginInfo' + summary: User login + tags: + - Auth + /auth/logout: + post: + description: 用户登出 + responses: + "200": + description: OK + security: + - ApiKeyAuth: [] + summary: User logout + tags: + - Auth + /auth/mfalogin: + post: + consumes: + - application/json + description: 用户 mfa 登录 + parameters: + - description: request + in: body + name: request + required: true + schema: + $ref: '#/definitions/dto.MFALogin' + responses: + "200": + description: OK + headers: + EntranceCode: + description: 安全入口 + type: string + schema: + $ref: '#/definitions/dto.UserLoginInfo' + summary: User login with mfa + tags: + - Auth + /containers: + post: + consumes: + - application/json + description: 创建容器 + parameters: + - description: request + in: body + name: request + required: true + schema: + $ref: '#/definitions/dto.ContainerOperate' + responses: + "200": + description: OK + security: + - ApiKeyAuth: [] + summary: Create container + tags: + - Container + x-panel-log: + BeforeFunctions: [] + bodyKeys: + - name + - image + formatEN: create container [name][image] + formatZH: 创建容器 [name][image] + paramKeys: [] + /containers/clean/log: + post: + consumes: + - application/json + description: 清理容器日志 + parameters: + - description: request + in: body + name: request + required: true + schema: + $ref: '#/definitions/dto.OperationWithName' + responses: + "200": + description: OK + security: + - ApiKeyAuth: [] + summary: Clean container log + tags: + - Container + x-panel-log: + BeforeFunctions: [] + bodyKeys: + - name + formatEN: clean container [name] logs + formatZH: 清理容器 [name] 日志 + paramKeys: [] + /containers/commit: + post: + consumes: + - application/json + description: 容器提交生成新镜像 + parameters: + - description: request + in: body + name: request + required: true + schema: + $ref: '#/definitions/dto.ContainerCommit' + responses: + "200": + description: OK + summary: Commit Container + tags: + - Container + /containers/compose: + post: + consumes: + - application/json + description: 创建容器编排 + parameters: + - description: request + in: body + name: request + required: true + schema: + $ref: '#/definitions/dto.ComposeCreate' + responses: + "200": + description: OK + security: + - ApiKeyAuth: [] + summary: Create compose + tags: + - Container Compose + x-panel-log: + BeforeFunctions: [] + bodyKeys: + - name + formatEN: create compose [name] + formatZH: 创建 compose [name] + paramKeys: [] + /containers/compose/operate: + post: + consumes: + - application/json + description: 容器编排操作 + parameters: + - description: request + in: body + name: request + required: true + schema: + $ref: '#/definitions/dto.ComposeOperation' + responses: + "200": + description: OK + security: + - ApiKeyAuth: [] + summary: Operate compose + tags: + - Container Compose + x-panel-log: + BeforeFunctions: [] + bodyKeys: + - name + - operation + formatEN: compose [operation] [name] + formatZH: compose [operation] [name] + paramKeys: [] + /containers/compose/search: + post: + consumes: + - application/json + description: 获取编排列表分页 + parameters: + - description: request + in: body + name: request + required: true + schema: + $ref: '#/definitions/dto.SearchWithPage' + responses: + "200": + description: OK + schema: + $ref: '#/definitions/dto.PageResult' + security: + - ApiKeyAuth: [] + summary: Page composes + tags: + - Container Compose + /containers/compose/search/log: + get: + description: docker-compose 日志 + parameters: + - description: compose 文件地址 + in: query + name: compose + type: string + - description: 时间筛选 + in: query + name: since + type: string + - description: 是否追踪 + in: query + name: follow + type: string + - description: 显示行号 + in: query + name: tail + type: string + responses: {} + security: + - ApiKeyAuth: [] + summary: Container Compose logs + tags: + - Container Compose + /containers/compose/test: + post: + consumes: + - application/json + description: 测试 compose 是否可用 + parameters: + - description: request + in: body + name: request + required: true + schema: + $ref: '#/definitions/dto.ComposeCreate' + responses: + "200": + description: OK + security: + - ApiKeyAuth: [] + summary: Test compose + tags: + - Container Compose + x-panel-log: + BeforeFunctions: [] + bodyKeys: + - name + formatEN: check compose [name] + formatZH: 检测 compose [name] 格式 + paramKeys: [] + /containers/compose/update: + post: + consumes: + - application/json + description: 更新容器编排 + parameters: + - description: request + in: body + name: request + required: true + schema: + $ref: '#/definitions/dto.ComposeUpdate' + responses: + "200": + description: OK + security: + - ApiKeyAuth: [] + summary: Update compose + tags: + - Container Compose + x-panel-log: + BeforeFunctions: [] + bodyKeys: + - name + formatEN: update compose information [name] + formatZH: 更新 compose [name] + paramKeys: [] + /containers/daemonjson: + get: + description: 获取 docker 配置信息 + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/dto.DaemonJsonConf' + security: + - ApiKeyAuth: [] + summary: Load docker daemon.json + tags: + - Container Docker + /containers/daemonjson/file: + get: + description: 获取 docker 配置信息(表单) + produces: + - application/json + responses: + "200": + description: OK + schema: + type: string + security: + - ApiKeyAuth: [] + summary: Load docker daemon.json + tags: + - Container Docker + /containers/daemonjson/update: + post: + consumes: + - application/json + description: 修改 docker 配置信息 + parameters: + - description: request + in: body + name: request + required: true + schema: + $ref: '#/definitions/dto.SettingUpdate' + responses: + "200": + description: OK + security: + - ApiKeyAuth: [] + summary: Update docker daemon.json + tags: + - Container Docker + x-panel-log: + BeforeFunctions: [] + bodyKeys: + - key + - value + formatEN: Updated configuration [key] + formatZH: 更新配置 [key] + paramKeys: [] + /containers/daemonjson/update/byfile: + post: + consumes: + - application/json + description: 上传替换 docker 配置文件 + parameters: + - description: request + in: body + name: request + required: true + schema: + $ref: '#/definitions/dto.DaemonJsonUpdateByFile' + responses: + "200": + description: OK + security: + - ApiKeyAuth: [] + summary: Update docker daemon.json by upload file + tags: + - Container Docker + x-panel-log: + BeforeFunctions: [] + bodyKeys: [] + formatEN: Updated configuration file + formatZH: 更新配置文件 + paramKeys: [] + /containers/docker/operate: + post: + consumes: + - application/json + description: Docker 操作 + parameters: + - description: request + in: body + name: request + required: true + schema: + $ref: '#/definitions/dto.DockerOperation' + responses: + "200": + description: OK + security: + - ApiKeyAuth: [] + summary: Operate docker + tags: + - Container Docker + x-panel-log: + BeforeFunctions: [] + bodyKeys: + - operation + formatEN: '[operation] docker service' + formatZH: docker 服务 [operation] + paramKeys: [] + /containers/docker/status: + get: + description: 获取 docker 服务状态 + produces: + - application/json + responses: + "200": + description: OK + schema: + type: string + security: + - ApiKeyAuth: [] + summary: Load docker status + tags: + - Container Docker + /containers/download/log: + post: + description: 下载容器日志 + responses: {} + /containers/image: + get: + description: 获取镜像名称列表 + produces: + - application/json + responses: + "200": + description: OK + schema: + items: + $ref: '#/definitions/dto.Options' + type: array + security: + - ApiKeyAuth: [] + summary: load images options + tags: + - Container Image + /containers/image/all: + get: + description: 获取所有镜像列表 + produces: + - application/json + responses: + "200": + description: OK + schema: + items: + $ref: '#/definitions/dto.ImageInfo' + type: array + security: + - ApiKeyAuth: [] + summary: List all images + tags: + - Container Image + /containers/image/build: + post: + consumes: + - application/json + description: 构建镜像 + parameters: + - description: request + in: body + name: request + required: true + schema: + $ref: '#/definitions/dto.ImageBuild' + responses: + "200": + description: OK + schema: + type: string + security: + - ApiKeyAuth: [] + summary: Build image + tags: + - Container Image + x-panel-log: + BeforeFunctions: [] + bodyKeys: + - name + formatEN: build image [name] + formatZH: 构建镜像 [name] + paramKeys: [] + /containers/image/load: + post: + consumes: + - application/json + description: 导入镜像 + parameters: + - description: request + in: body + name: request + required: true + schema: + $ref: '#/definitions/dto.ImageLoad' + responses: + "200": + description: OK + security: + - ApiKeyAuth: [] + summary: Load image + tags: + - Container Image + x-panel-log: + BeforeFunctions: [] + bodyKeys: + - path + formatEN: load image from [path] + formatZH: 从 [path] 加载镜像 + paramKeys: [] + /containers/image/pull: + post: + consumes: + - application/json + description: 拉取镜像 + parameters: + - description: request + in: body + name: request + required: true + schema: + $ref: '#/definitions/dto.ImagePull' + responses: + "200": + description: OK + schema: + type: string + security: + - ApiKeyAuth: [] + summary: Pull image + tags: + - Container Image + x-panel-log: + BeforeFunctions: + - db: image_repos + input_column: id + input_value: repoID + isList: false + output_column: name + output_value: reponame + bodyKeys: + - repoID + - imageName + formatEN: image pull [reponame][imageName] + formatZH: 镜像拉取 [reponame][imageName] + paramKeys: [] + /containers/image/push: + post: + consumes: + - application/json + description: 推送镜像 + parameters: + - description: request + in: body + name: request + required: true + schema: + $ref: '#/definitions/dto.ImagePush' + responses: + "200": + description: OK + schema: + type: string + security: + - ApiKeyAuth: [] + summary: Push image + tags: + - Container Image + x-panel-log: + BeforeFunctions: + - db: image_repos + input_column: id + input_value: repoID + isList: false + output_column: name + output_value: reponame + bodyKeys: + - repoID + - tagName + - name + formatEN: push [tagName] to [reponame][name] + formatZH: '[tagName] 推送到 [reponame][name]' + paramKeys: [] + /containers/image/remove: + post: + consumes: + - application/json + description: 删除镜像 + parameters: + - description: request + in: body + name: request + required: true + schema: + $ref: '#/definitions/dto.BatchDelete' + responses: + "200": + description: OK + security: + - ApiKeyAuth: [] + summary: Delete image + tags: + - Container Image + x-panel-log: + BeforeFunctions: [] + bodyKeys: + - names + formatEN: remove image [names] + formatZH: 移除镜像 [names] + paramKeys: [] + /containers/image/save: + post: + consumes: + - application/json + description: 导出镜像 + parameters: + - description: request + in: body + name: request + required: true + schema: + $ref: '#/definitions/dto.ImageSave' + responses: + "200": + description: OK + security: + - ApiKeyAuth: [] + summary: Save image + tags: + - Container Image + x-panel-log: + BeforeFunctions: [] + bodyKeys: + - tagName + - path + - name + formatEN: save [tagName] as [path]/[name] + formatZH: 保留 [tagName] 为 [path]/[name] + paramKeys: [] + /containers/image/search: + post: + consumes: + - application/json + description: 获取镜像列表分页 + parameters: + - description: request + in: body + name: request + required: true + schema: + $ref: '#/definitions/dto.SearchWithPage' + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/dto.PageResult' + security: + - ApiKeyAuth: [] + summary: Page images + tags: + - Container Image + /containers/image/tag: + post: + consumes: + - application/json + description: Tag 镜像 + parameters: + - description: request + in: body + name: request + required: true + schema: + $ref: '#/definitions/dto.ImageTag' + responses: + "200": + description: OK + security: + - ApiKeyAuth: [] + summary: Tag image + tags: + - Container Image + x-panel-log: + BeforeFunctions: + - db: image_repos + input_column: id + input_value: repoID + isList: false + output_column: name + output_value: reponame + bodyKeys: + - repoID + - targetName + formatEN: tag image [reponame][targetName] + formatZH: tag 镜像 [reponame][targetName] + paramKeys: [] + /containers/info: + post: + consumes: + - application/json + description: 获取容器表单信息 + parameters: + - description: request + in: body + name: request + required: true + schema: + $ref: '#/definitions/dto.OperationWithName' + responses: + "200": + description: OK + schema: + $ref: '#/definitions/dto.ContainerOperate' + security: + - ApiKeyAuth: [] + summary: Load container info + tags: + - Container + /containers/inspect: + post: + consumes: + - application/json + description: 容器详情 + parameters: + - description: request + in: body + name: request + required: true + schema: + $ref: '#/definitions/dto.InspectReq' + responses: + "200": + description: OK + schema: + type: string + security: + - ApiKeyAuth: [] + summary: Container inspect + tags: + - Container + /containers/ipv6option/update: + post: + consumes: + - application/json + description: 修改 docker ipv6 配置 + parameters: + - description: request + in: body + name: request + required: true + schema: + $ref: '#/definitions/dto.LogOption' + responses: + "200": + description: OK + security: + - ApiKeyAuth: [] + summary: Update docker daemon.json ipv6 option + tags: + - Container Docker + x-panel-log: + BeforeFunctions: [] + bodyKeys: [] + formatEN: Updated the ipv6 option + formatZH: 更新 ipv6 配置 + paramKeys: [] + /containers/limit: + get: + description: 获取容器限制 + responses: + "200": + description: OK + schema: + $ref: '#/definitions/dto.ResourceLimit' + security: + - ApiKeyAuth: [] + summary: Load container limits + /containers/list: + post: + consumes: + - application/json + description: 获取容器名称 + produces: + - application/json + responses: + "200": + description: OK + security: + - ApiKeyAuth: [] + summary: List containers + tags: + - Container + /containers/list/stats: + get: + description: 获取容器列表资源占用 + responses: + "200": + description: OK + schema: + items: + $ref: '#/definitions/dto.ContainerListStats' + type: array + security: + - ApiKeyAuth: [] + summary: Load container stats + /containers/load/log: + post: + consumes: + - application/json + description: 获取容器操作日志 + parameters: + - description: request + in: body + name: request + required: true + schema: + $ref: '#/definitions/dto.OperationWithNameAndType' + responses: + "200": + description: OK + security: + - ApiKeyAuth: [] + summary: Load container log + tags: + - Container + /containers/logoption/update: + post: + consumes: + - application/json + description: 修改 docker 日志配置 + parameters: + - description: request + in: body + name: request + required: true + schema: + $ref: '#/definitions/dto.LogOption' + responses: + "200": + description: OK + security: + - ApiKeyAuth: [] + summary: Update docker daemon.json log option + tags: + - Container Docker + x-panel-log: + BeforeFunctions: [] + bodyKeys: [] + formatEN: Updated the log option + formatZH: 更新日志配置 + paramKeys: [] + /containers/network: + get: + consumes: + - application/json + description: 获取容器网络列表 + produces: + - application/json + responses: + "200": + description: OK + schema: + items: + $ref: '#/definitions/dto.Options' + type: array + security: + - ApiKeyAuth: [] + summary: List networks + tags: + - Container Network + post: + consumes: + - application/json + description: 创建容器网络 + parameters: + - description: request + in: body + name: request + required: true + schema: + $ref: '#/definitions/dto.NetworkCreate' + responses: + "200": + description: OK + security: + - ApiKeyAuth: [] + summary: Create network + tags: + - Container Network + x-panel-log: + BeforeFunctions: [] + bodyKeys: + - name + formatEN: create container network [name] + formatZH: 创建容器网络 name + paramKeys: [] + /containers/network/del: + post: + consumes: + - application/json + description: 删除容器网络 + parameters: + - description: request + in: body + name: request + required: true + schema: + $ref: '#/definitions/dto.BatchDelete' + responses: + "200": + description: OK + security: + - ApiKeyAuth: [] + summary: Delete network + tags: + - Container Network + x-panel-log: + BeforeFunctions: [] + bodyKeys: + - names + formatEN: delete container network [names] + formatZH: 删除容器网络 [names] + paramKeys: [] + /containers/network/search: + post: + consumes: + - application/json + description: 获取容器网络列表分页 + parameters: + - description: request + in: body + name: request + required: true + schema: + $ref: '#/definitions/dto.SearchWithPage' + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/dto.PageResult' + security: + - ApiKeyAuth: [] + summary: Page networks + tags: + - Container Network + /containers/operate: + post: + consumes: + - application/json + description: 容器操作 + parameters: + - description: request + in: body + name: request + required: true + schema: + $ref: '#/definitions/dto.ContainerOperation' + responses: + "200": + description: OK + security: + - ApiKeyAuth: [] + summary: Operate Container + tags: + - Container + x-panel-log: + BeforeFunctions: [] + bodyKeys: + - names + - operation + formatEN: container [operation] [names] + formatZH: 容器 [names] 执行 [operation] + paramKeys: [] + /containers/prune: + post: + consumes: + - application/json + description: 容器清理 + parameters: + - description: request + in: body + name: request + required: true + schema: + $ref: '#/definitions/dto.ContainerPrune' + responses: + "200": + description: OK + schema: + $ref: '#/definitions/dto.ContainerPruneReport' + security: + - ApiKeyAuth: [] + summary: Clean container + tags: + - Container + x-panel-log: + BeforeFunctions: [] + bodyKeys: + - pruneType + formatEN: clean container [pruneType] + formatZH: 清理容器 [pruneType] + paramKeys: [] + /containers/rename: + post: + consumes: + - application/json + description: 容器重命名 + parameters: + - description: request + in: body + name: request + required: true + schema: + $ref: '#/definitions/dto.ContainerRename' + responses: + "200": + description: OK + security: + - ApiKeyAuth: [] + summary: Rename Container + tags: + - Container + x-panel-log: + BeforeFunctions: [] + bodyKeys: + - name + - newName + formatEN: rename container [name] => [newName] + formatZH: 容器重命名 [name] => [newName] + paramKeys: [] + /containers/repo: + get: + description: 获取镜像仓库列表 + produces: + - application/json + responses: + "200": + description: OK + schema: + items: + $ref: '#/definitions/dto.ImageRepoOption' + type: array + security: + - ApiKeyAuth: [] + summary: List image repos + tags: + - Container Image-repo + post: + consumes: + - application/json + description: 创建镜像仓库 + parameters: + - description: request + in: body + name: request + required: true + schema: + $ref: '#/definitions/dto.ImageRepoDelete' + produces: + - application/json + responses: + "200": + description: OK + security: + - ApiKeyAuth: [] + summary: Create image repo + tags: + - Container Image-repo + x-panel-log: + BeforeFunctions: [] + bodyKeys: + - name + formatEN: create image repo [name] + formatZH: 创建镜像仓库 [name] + paramKeys: [] + /containers/repo/del: + post: + consumes: + - application/json + description: 删除镜像仓库 + parameters: + - description: request + in: body + name: request + required: true + schema: + $ref: '#/definitions/dto.ImageRepoDelete' + produces: + - application/json + responses: + "200": + description: OK + security: + - ApiKeyAuth: [] + summary: Delete image repo + tags: + - Container Image-repo + x-panel-log: + BeforeFunctions: + - db: image_repos + input_column: id + input_value: ids + isList: true + output_column: name + output_value: names + bodyKeys: + - ids + formatEN: delete image repo [names] + formatZH: 删除镜像仓库 [names] + paramKeys: [] + /containers/repo/search: + post: + consumes: + - application/json + description: 获取镜像仓库列表分页 + parameters: + - description: request + in: body + name: request + required: true + schema: + $ref: '#/definitions/dto.SearchWithPage' + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/dto.PageResult' + security: + - ApiKeyAuth: [] + summary: Page image repos + tags: + - Container Image-repo + /containers/repo/status: + get: + consumes: + - application/json + description: 获取 docker 仓库状态 + parameters: + - description: request + in: body + name: request + required: true + schema: + $ref: '#/definitions/dto.OperateByID' + produces: + - application/json + responses: + "200": + description: OK + security: + - ApiKeyAuth: [] + summary: Load repo status + tags: + - Container Image-repo + /containers/repo/update: + post: + consumes: + - application/json + description: 更新镜像仓库 + parameters: + - description: request + in: body + name: request + required: true + schema: + $ref: '#/definitions/dto.ImageRepoUpdate' + produces: + - application/json + responses: + "200": + description: OK + security: + - ApiKeyAuth: [] + summary: Update image repo + tags: + - Container Image-repo + x-panel-log: + BeforeFunctions: + - db: image_repos + input_column: id + input_value: id + isList: false + output_column: name + output_value: name + bodyKeys: + - id + formatEN: update image repo information [name] + formatZH: 更新镜像仓库 [name] + paramKeys: [] + /containers/search: + post: + consumes: + - application/json + description: 获取容器列表分页 + parameters: + - description: request + in: body + name: request + required: true + schema: + $ref: '#/definitions/dto.PageContainer' + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/dto.PageResult' + security: + - ApiKeyAuth: [] + summary: Page containers + tags: + - Container + /containers/search/log: + post: + description: 容器日志 + parameters: + - description: 容器名称 + in: query + name: container + type: string + - description: 时间筛选 + in: query + name: since + type: string + - description: 是否追踪 + in: query + name: follow + type: string + - description: 显示行号 + in: query + name: tail + type: string + responses: {} + security: + - ApiKeyAuth: [] + summary: Container logs + tags: + - Container + /containers/stats/:id: + get: + description: 容器监控信息 + parameters: + - description: 容器id + in: path + name: id + required: true + type: integer + responses: + "200": + description: OK + schema: + $ref: '#/definitions/dto.ContainerStats' + security: + - ApiKeyAuth: [] + summary: Container stats + tags: + - Container + /containers/template: + get: + description: 获取容器编排模版列表 + produces: + - application/json + responses: + "200": + description: OK + schema: + items: + $ref: '#/definitions/dto.ComposeTemplateInfo' + type: array + security: + - ApiKeyAuth: [] + summary: List compose templates + tags: + - Container Compose-template + post: + consumes: + - application/json + description: 创建容器编排模版 + parameters: + - description: request + in: body + name: request + required: true + schema: + $ref: '#/definitions/dto.ComposeTemplateCreate' + responses: + "200": + description: OK + security: + - ApiKeyAuth: [] + summary: Create compose template + tags: + - Container Compose-template + x-panel-log: + BeforeFunctions: [] + bodyKeys: + - name + formatEN: create compose template [name] + formatZH: 创建 compose 模版 [name] + paramKeys: [] + /containers/template/del: + post: + consumes: + - application/json + description: 删除容器编排模版 + parameters: + - description: request + in: body + name: request + required: true + schema: + $ref: '#/definitions/dto.BatchDelete' + responses: + "200": + description: OK + security: + - ApiKeyAuth: [] + summary: Delete compose template + tags: + - Container Compose-template + x-panel-log: + BeforeFunctions: + - db: compose_templates + input_column: id + input_value: ids + isList: true + output_column: name + output_value: names + bodyKeys: + - ids + formatEN: delete compose template [names] + formatZH: 删除 compose 模版 [names] + paramKeys: [] + /containers/template/search: + post: + consumes: + - application/json + description: 获取容器编排模版列表分页 + parameters: + - description: request + in: body + name: request + required: true + schema: + $ref: '#/definitions/dto.SearchWithPage' + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/dto.PageResult' + security: + - ApiKeyAuth: [] + summary: Page compose templates + tags: + - Container Compose-template + /containers/template/update: + post: + consumes: + - application/json + description: 更新容器编排模版 + parameters: + - description: request + in: body + name: request + required: true + schema: + $ref: '#/definitions/dto.ComposeTemplateUpdate' + responses: + "200": + description: OK + security: + - ApiKeyAuth: [] + summary: Update compose template + tags: + - Container Compose-template + x-panel-log: + BeforeFunctions: + - db: compose_templates + input_column: id + input_value: id + isList: false + output_column: name + output_value: name + bodyKeys: + - id + formatEN: update compose template information [name] + formatZH: 更新 compose 模版 [name] + paramKeys: [] + /containers/update: + post: + consumes: + - application/json + description: 更新容器 + parameters: + - description: request + in: body + name: request + required: true + schema: + $ref: '#/definitions/dto.ContainerOperate' + responses: + "200": + description: OK + security: + - ApiKeyAuth: [] + summary: Update container + tags: + - Container + x-panel-log: + BeforeFunctions: [] + bodyKeys: + - name + - image + formatEN: update container [name][image] + formatZH: 更新容器 [name][image] + paramKeys: [] + /containers/upgrade: + post: + consumes: + - application/json + description: 更新容器镜像 + parameters: + - description: request + in: body + name: request + required: true + schema: + $ref: '#/definitions/dto.ContainerUpgrade' + responses: + "200": + description: OK + security: + - ApiKeyAuth: [] + summary: Upgrade container + tags: + - Container + x-panel-log: + BeforeFunctions: [] + bodyKeys: + - name + - image + formatEN: upgrade container image [name][image] + formatZH: 更新容器镜像 [name][image] + paramKeys: [] + /containers/volume: + get: + consumes: + - application/json + description: 获取容器存储卷列表 + produces: + - application/json + responses: + "200": + description: OK + schema: + items: + $ref: '#/definitions/dto.Options' + type: array + security: + - ApiKeyAuth: [] + summary: List volumes + tags: + - Container Volume + post: + consumes: + - application/json + description: 创建容器存储卷 + parameters: + - description: request + in: body + name: request + required: true + schema: + $ref: '#/definitions/dto.VolumeCreate' + responses: + "200": + description: OK + security: + - ApiKeyAuth: [] + summary: Create volume + tags: + - Container Volume + x-panel-log: + BeforeFunctions: [] + bodyKeys: + - name + formatEN: create container volume [name] + formatZH: 创建容器存储卷 [name] + paramKeys: [] + /containers/volume/del: + post: + consumes: + - application/json + description: 删除容器存储卷 + parameters: + - description: request + in: body + name: request + required: true + schema: + $ref: '#/definitions/dto.BatchDelete' + responses: + "200": + description: OK + security: + - ApiKeyAuth: [] + summary: Delete volume + tags: + - Container Volume + x-panel-log: + BeforeFunctions: [] + bodyKeys: + - names + formatEN: delete container volume [names] + formatZH: 删除容器存储卷 [names] + paramKeys: [] + /containers/volume/search: + post: + consumes: + - application/json + description: 获取容器存储卷分页 + parameters: + - description: request + in: body + name: request + required: true + schema: + $ref: '#/definitions/dto.SearchWithPage' + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/dto.PageResult' + security: + - ApiKeyAuth: [] + summary: Page volumes + tags: + - Container Volume + /cronjobs: + post: + consumes: + - application/json + description: 创建计划任务 + parameters: + - description: request + in: body + name: request + required: true + schema: + $ref: '#/definitions/dto.CronjobCreate' + responses: + "200": + description: OK + security: + - ApiKeyAuth: [] + summary: Create cronjob + tags: + - Cronjob + x-panel-log: + BeforeFunctions: [] + bodyKeys: + - type + - name + formatEN: create cronjob [type][name] + formatZH: 创建计划任务 [type][name] + paramKeys: [] + /cronjobs/del: + post: + consumes: + - application/json + description: 删除计划任务 + parameters: + - description: request + in: body + name: request + required: true + schema: + $ref: '#/definitions/dto.CronjobBatchDelete' + responses: + "200": + description: OK + security: + - ApiKeyAuth: [] + summary: Delete cronjob + tags: + - Cronjob + x-panel-log: + BeforeFunctions: + - db: cronjobs + input_column: id + input_value: ids + isList: true + output_column: name + output_value: names + bodyKeys: + - ids + formatEN: delete cronjob [names] + formatZH: 删除计划任务 [names] + paramKeys: [] + /cronjobs/download: + post: + consumes: + - application/json + description: 下载计划任务记录 + parameters: + - description: request + in: body + name: request + required: true + schema: + $ref: '#/definitions/dto.CronjobDownload' + responses: + "200": + description: OK + security: + - ApiKeyAuth: [] + summary: Download cronjob records + tags: + - Cronjob + x-panel-log: + BeforeFunctions: + - db: job_records + input_column: id + input_value: recordID + isList: false + output_column: file + output_value: file + bodyKeys: + - recordID + formatEN: download the cronjob record [file] + formatZH: 下载计划任务记录 [file] + paramKeys: [] + /cronjobs/handle: + post: + consumes: + - application/json + description: 手动执行计划任务 + parameters: + - description: request + in: body + name: request + required: true + schema: + $ref: '#/definitions/dto.OperateByID' + responses: + "200": + description: OK + security: + - ApiKeyAuth: [] + summary: Handle cronjob once + tags: + - Cronjob + x-panel-log: + BeforeFunctions: + - db: cronjobs + input_column: id + input_value: id + isList: false + output_column: name + output_value: name + bodyKeys: + - id + formatEN: manually execute the cronjob [name] + formatZH: 手动执行计划任务 [name] + paramKeys: [] + /cronjobs/records/clean: + post: + consumes: + - application/json + description: 清空计划任务记录 + parameters: + - description: request + in: body + name: request + required: true + schema: + $ref: '#/definitions/dto.CronjobClean' + responses: + "200": + description: OK + security: + - ApiKeyAuth: [] + summary: Clean job records + tags: + - Cronjob + x-panel-log: + BeforeFunctions: + - db: cronjobs + input_column: id + input_value: id + isList: false + output_column: name + output_value: name + bodyKeys: + - id + formatEN: clean cronjob [name] records + formatZH: 清空计划任务记录 [name] + paramKeys: [] + /cronjobs/records/log: + post: + consumes: + - application/json + description: 获取计划任务记录日志 + parameters: + - description: request + in: body + name: request + required: true + schema: + $ref: '#/definitions/dto.OperateByID' + responses: + "200": + description: OK + security: + - ApiKeyAuth: [] + summary: Load Cronjob record log + tags: + - Cronjob + /cronjobs/search: + post: + consumes: + - application/json + description: 获取计划任务分页 + parameters: + - description: request + in: body + name: request + required: true + schema: + $ref: '#/definitions/dto.PageCronjob' + responses: + "200": + description: OK + schema: + $ref: '#/definitions/dto.PageResult' + security: + - ApiKeyAuth: [] + summary: Page cronjobs + tags: + - Cronjob + /cronjobs/search/records: + post: + consumes: + - application/json + description: 获取计划任务记录 + parameters: + - description: request + in: body + name: request + required: true + schema: + $ref: '#/definitions/dto.SearchRecord' + responses: + "200": + description: OK + schema: + $ref: '#/definitions/dto.PageResult' + security: + - ApiKeyAuth: [] + summary: Page job records + tags: + - Cronjob + /cronjobs/status: + post: + consumes: + - application/json + description: 更新计划任务状态 + parameters: + - description: request + in: body + name: request + required: true + schema: + $ref: '#/definitions/dto.CronjobUpdateStatus' + responses: + "200": + description: OK + security: + - ApiKeyAuth: [] + summary: Update cronjob status + tags: + - Cronjob + x-panel-log: + BeforeFunctions: + - db: cronjobs + input_column: id + input_value: id + isList: false + output_column: name + output_value: name + bodyKeys: + - id + - status + formatEN: change the status of cronjob [name] to [status]. + formatZH: 修改计划任务 [name] 状态为 [status] + paramKeys: [] + /cronjobs/update: + post: + consumes: + - application/json + description: 更新计划任务 + parameters: + - description: request + in: body + name: request + required: true + schema: + $ref: '#/definitions/dto.CronjobUpdate' + responses: + "200": + description: OK + security: + - ApiKeyAuth: [] + summary: Update cronjob + tags: + - Cronjob + x-panel-log: + BeforeFunctions: + - db: cronjobs + input_column: id + input_value: id + isList: false + output_column: name + output_value: name + bodyKeys: + - id + formatEN: update cronjob [name] + formatZH: 更新计划任务 [name] + paramKeys: [] + /dashboard/base/:ioOption/:netOption: + get: + consumes: + - application/json + description: 获取首页基础数据 + parameters: + - description: request + in: path + name: ioOption + required: true + type: string + - description: request + in: path + name: netOption + required: true + type: string + responses: + "200": + description: OK + schema: + $ref: '#/definitions/dto.DashboardBase' + security: + - ApiKeyAuth: [] + 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: + - application/json + description: 获取首页实时数据 + parameters: + - description: request + in: path + name: ioOption + required: true + type: string + - description: request + in: path + name: netOption + required: true + type: string + responses: + "200": + description: OK + schema: + $ref: '#/definitions/dto.DashboardCurrent' + security: + - ApiKeyAuth: [] + summary: Load dashboard current info + tags: + - Dashboard + /dashboard/system/restart/:operation: + post: + consumes: + - application/json + description: 重启服务器/面板 + parameters: + - description: request + in: path + name: operation + required: true + type: string + responses: + "200": + description: OK + security: + - ApiKeyAuth: [] + summary: System restart + tags: + - Dashboard + /databases: + post: + consumes: + - application/json + description: 创建 mysql 数据库 + parameters: + - description: request + in: body + name: request + required: true + schema: + $ref: '#/definitions/dto.MysqlDBCreate' + responses: + "200": + description: OK + security: + - ApiKeyAuth: [] + summary: Create mysql database + tags: + - Database Mysql + x-panel-log: + BeforeFunctions: [] + bodyKeys: + - name + formatEN: create mysql database [name] + formatZH: 创建 mysql 数据库 [name] + paramKeys: [] + /databases/bind: + post: + consumes: + - application/json + description: 绑定 mysql 数据库用户 + parameters: + - description: request + in: body + name: request + required: true + schema: + $ref: '#/definitions/dto.BindUser' + responses: + "200": + description: OK + security: + - ApiKeyAuth: [] + summary: Bind user of mysql database + tags: + - Database Mysql + x-panel-log: + BeforeFunctions: [] + bodyKeys: + - database + - username + formatEN: bind mysql database [database] [username] + formatZH: 绑定 mysql 数据库名 [database] [username] + paramKeys: [] + /databases/change/access: + post: + consumes: + - application/json + description: 修改 mysql 访问权限 + parameters: + - description: request + in: body + name: request + required: true + schema: + $ref: '#/definitions/dto.ChangeDBInfo' + responses: + "200": + description: OK + security: + - ApiKeyAuth: [] + summary: Change mysql access + tags: + - Database Mysql + x-panel-log: + BeforeFunctions: + - db: database_mysqls + input_column: id + input_value: id + isList: false + output_column: name + output_value: name + bodyKeys: + - id + formatEN: Update database [name] access + formatZH: 更新数据库 [name] 访问权限 + paramKeys: [] + /databases/change/password: + post: + consumes: + - application/json + description: 修改 mysql 密码 + parameters: + - description: request + in: body + name: request + required: true + schema: + $ref: '#/definitions/dto.ChangeDBInfo' + responses: + "200": + description: OK + security: + - ApiKeyAuth: [] + summary: Change mysql password + tags: + - Database Mysql + x-panel-log: + BeforeFunctions: + - db: database_mysqls + input_column: id + input_value: id + isList: false + output_column: name + output_value: name + bodyKeys: + - id + formatEN: Update database [name] password + formatZH: 更新数据库 [name] 密码 + paramKeys: [] + /databases/common/info: + post: + consumes: + - application/json + description: 获取数据库基础信息 + parameters: + - description: request + in: body + name: request + required: true + schema: + $ref: '#/definitions/dto.OperationWithNameAndType' + responses: + "200": + description: OK + schema: + $ref: '#/definitions/dto.DBBaseInfo' + security: + - ApiKeyAuth: [] + summary: Load base info + tags: + - Database Common + /databases/common/load/file: + post: + consumes: + - application/json + description: 获取数据库配置文件 + parameters: + - description: request + in: body + name: request + required: true + schema: + $ref: '#/definitions/dto.OperationWithNameAndType' + responses: + "200": + description: OK + security: + - ApiKeyAuth: [] + summary: Load Database conf + tags: + - Database Common + /databases/common/update/conf: + post: + consumes: + - application/json + description: 上传替换配置文件 + parameters: + - description: request + in: body + name: request + required: true + schema: + $ref: '#/definitions/dto.DBConfUpdateByFile' + responses: + "200": + description: OK + security: + - ApiKeyAuth: [] + summary: Update conf by upload file + tags: + - Database Common + x-panel-log: + BeforeFunctions: [] + bodyKeys: + - type + - database + formatEN: update the [type] [database] database configuration information + formatZH: 更新 [type] 数据库 [database] 配置信息 + paramKeys: [] + /databases/db: + post: + consumes: + - application/json + description: 创建远程数据库 + parameters: + - description: request + in: body + name: request + required: true + schema: + $ref: '#/definitions/dto.DatabaseCreate' + responses: + "200": + description: OK + security: + - ApiKeyAuth: [] + summary: Create database + tags: + - Database + x-panel-log: + BeforeFunctions: [] + bodyKeys: + - name + - type + formatEN: create database [name][type] + formatZH: 创建远程数据库 [name][type] + paramKeys: [] + /databases/db/:name: + get: + description: 获取远程数据库 + responses: + "200": + description: OK + schema: + $ref: '#/definitions/dto.DatabaseInfo' + security: + - ApiKeyAuth: [] + summary: Get databases + tags: + - Database + /databases/db/check: + post: + consumes: + - application/json + description: 检测远程数据库连接性 + parameters: + - description: request + in: body + name: request + required: true + schema: + $ref: '#/definitions/dto.DatabaseCreate' + responses: + "200": + description: OK + security: + - ApiKeyAuth: [] + summary: Check database + tags: + - Database + x-panel-log: + BeforeFunctions: [] + bodyKeys: + - name + - type + formatEN: check if database [name][type] is connectable + formatZH: 检测远程数据库 [name][type] 连接性 + paramKeys: [] + /databases/db/del: + post: + consumes: + - application/json + description: 删除远程数据库 + parameters: + - description: request + in: body + name: request + required: true + schema: + $ref: '#/definitions/dto.DatabaseDelete' + responses: + "200": + description: OK + security: + - ApiKeyAuth: [] + summary: Delete database + tags: + - Database + x-panel-log: + BeforeFunctions: + - db: databases + input_column: id + input_value: ids + isList: true + output_column: name + output_value: names + bodyKeys: + - ids + formatEN: delete database [names] + formatZH: 删除远程数据库 [names] + paramKeys: [] + /databases/db/item/:type: + get: + description: 获取数据库列表 + responses: + "200": + description: OK + schema: + items: + $ref: '#/definitions/dto.DatabaseItem' + type: array + security: + - ApiKeyAuth: [] + summary: List databases + tags: + - Database + /databases/db/list/:type: + get: + description: 获取远程数据库列表 + responses: + "200": + description: OK + schema: + items: + $ref: '#/definitions/dto.DatabaseOption' + type: array + security: + - ApiKeyAuth: [] + summary: List databases + tags: + - Database + /databases/db/search: + post: + consumes: + - application/json + description: 获取远程数据库列表分页 + parameters: + - description: request + in: body + name: request + required: true + schema: + $ref: '#/definitions/dto.DatabaseSearch' + responses: + "200": + description: OK + schema: + $ref: '#/definitions/dto.PageResult' + security: + - ApiKeyAuth: [] + summary: Page databases + tags: + - Database + /databases/db/update: + post: + consumes: + - application/json + description: 更新远程数据库 + parameters: + - description: request + in: body + name: request + required: true + schema: + $ref: '#/definitions/dto.DatabaseUpdate' + responses: + "200": + description: OK + security: + - ApiKeyAuth: [] + summary: Update database + tags: + - Database + x-panel-log: + BeforeFunctions: [] + bodyKeys: + - name + formatEN: update database [name] + formatZH: 更新远程数据库 [name] + paramKeys: [] + /databases/del: + post: + consumes: + - application/json + description: 删除 mysql 数据库 + parameters: + - description: request + in: body + name: request + required: true + schema: + $ref: '#/definitions/dto.MysqlDBDelete' + responses: + "200": + description: OK + security: + - ApiKeyAuth: [] + summary: Delete mysql database + tags: + - Database Mysql + x-panel-log: + BeforeFunctions: + - db: database_mysqls + input_column: id + input_value: id + isList: false + output_column: name + output_value: name + bodyKeys: + - id + formatEN: delete mysql database [name] + formatZH: 删除 mysql 数据库 [name] + paramKeys: [] + /databases/del/check: + post: + consumes: + - application/json + description: Mysql 数据库删除前检查 + parameters: + - description: request + in: body + name: request + required: true + schema: + $ref: '#/definitions/dto.MysqlDBDeleteCheck' + responses: + "200": + description: OK + schema: + items: + type: string + type: array + security: + - ApiKeyAuth: [] + summary: Check before delete mysql database + tags: + - Database Mysql + /databases/description/update: + post: + consumes: + - application/json + description: 更新 mysql 数据库库描述信息 + parameters: + - description: request + in: body + name: request + required: true + schema: + $ref: '#/definitions/dto.UpdateDescription' + responses: + "200": + description: OK + security: + - ApiKeyAuth: [] + summary: Update mysql database description + tags: + - Database Mysql + x-panel-log: + BeforeFunctions: + - db: database_mysqls + input_column: id + input_value: id + isList: false + output_column: name + output_value: name + bodyKeys: + - id + - description + formatEN: The description of the mysql database [name] is modified => [description] + formatZH: mysql 数据库 [name] 描述信息修改 [description] + paramKeys: [] + /databases/load: + post: + consumes: + - application/json + description: 从服务器获取 + parameters: + - description: request + in: body + name: request + required: true + schema: + $ref: '#/definitions/dto.MysqlLoadDB' + responses: {} + security: + - ApiKeyAuth: [] + summary: Load mysql database from remote + tags: + - Database Mysql + /databases/options: + get: + consumes: + - application/json + description: 获取 mysql 数据库列表 + parameters: + - description: request + in: body + name: request + required: true + schema: + $ref: '#/definitions/dto.PageInfo' + responses: + "200": + description: OK + schema: + items: + $ref: '#/definitions/dto.MysqlOption' + type: array + security: + - ApiKeyAuth: [] + summary: List mysql database names + tags: + - Database Mysql + /databases/pg: + post: + consumes: + - application/json + description: 创建 postgresql 数据库 + parameters: + - description: request + in: body + name: request + required: true + schema: + $ref: '#/definitions/dto.PostgresqlDBCreate' + responses: + "200": + description: OK + security: + - ApiKeyAuth: [] + summary: Create postgresql database + tags: + - Database Postgresql + x-panel-log: + BeforeFunctions: [] + bodyKeys: + - name + formatEN: create postgresql database [name] + formatZH: 创建 postgresql 数据库 [name] + paramKeys: [] + /databases/pg/:database/load: + post: + consumes: + - application/json + description: 从服务器获取 + parameters: + - description: request + in: body + name: request + required: true + schema: + $ref: '#/definitions/dto.PostgresqlLoadDB' + responses: {} + security: + - ApiKeyAuth: [] + summary: Load postgresql database from remote + tags: + - Database Postgresql + /databases/pg/bind: + post: + consumes: + - application/json + description: 绑定 postgresql 数据库用户 + parameters: + - description: request + in: body + name: request + required: true + schema: + $ref: '#/definitions/dto.PostgresqlBindUser' + responses: + "200": + description: OK + security: + - ApiKeyAuth: [] + summary: Bind postgresql user + tags: + - Database Postgresql + x-panel-log: + BeforeFunctions: [] + bodyKeys: + - name + - username + formatEN: bind postgresql database [name] user [username] + formatZH: 绑定 postgresql 数据库 [name] 用户 [username] + paramKeys: [] + /databases/pg/del: + post: + consumes: + - application/json + description: 删除 postgresql 数据库 + parameters: + - description: request + in: body + name: request + required: true + schema: + $ref: '#/definitions/dto.PostgresqlDBDelete' + responses: + "200": + description: OK + security: + - ApiKeyAuth: [] + summary: Delete postgresql database + tags: + - Database Postgresql + x-panel-log: + BeforeFunctions: + - db: database_postgresqls + input_column: id + input_value: id + isList: false + output_column: name + output_value: name + bodyKeys: + - id + formatEN: delete postgresql database [name] + formatZH: 删除 postgresql 数据库 [name] + paramKeys: [] + /databases/pg/del/check: + post: + consumes: + - application/json + description: Postgresql 数据库删除前检查 + parameters: + - description: request + in: body + name: request + required: true + schema: + $ref: '#/definitions/dto.PostgresqlDBDeleteCheck' + responses: + "200": + description: OK + schema: + items: + type: string + type: array + security: + - ApiKeyAuth: [] + summary: Check before delete postgresql database + tags: + - Database Postgresql + /databases/pg/description: + post: + consumes: + - application/json + description: 更新 postgresql 数据库库描述信息 + parameters: + - description: request + in: body + name: request + required: true + schema: + $ref: '#/definitions/dto.UpdateDescription' + responses: + "200": + description: OK + security: + - ApiKeyAuth: [] + summary: Update postgresql database description + tags: + - Database Postgresql + x-panel-log: + BeforeFunctions: + - db: database_postgresqls + input_column: id + input_value: id + isList: false + output_column: name + output_value: name + bodyKeys: + - id + - description + formatEN: The description of the postgresql database [name] is modified => + [description] + formatZH: postgresql 数据库 [name] 描述信息修改 [description] + paramKeys: [] + /databases/pg/password: + post: + consumes: + - application/json + description: 修改 postgresql 密码 + parameters: + - description: request + in: body + name: request + required: true + schema: + $ref: '#/definitions/dto.ChangeDBInfo' + responses: + "200": + description: OK + security: + - ApiKeyAuth: [] + summary: Change postgresql password + tags: + - Database Postgresql + x-panel-log: + BeforeFunctions: + - db: database_postgresqls + input_column: id + input_value: id + isList: false + output_column: name + output_value: name + bodyKeys: + - id + formatEN: Update database [name] password + formatZH: 更新数据库 [name] 密码 + paramKeys: [] + /databases/pg/privileges: + post: + consumes: + - application/json + description: 修改 postgresql 用户权限 + parameters: + - description: request + in: body + name: request + required: true + schema: + $ref: '#/definitions/dto.ChangeDBInfo' + responses: + "200": + description: OK + security: + - ApiKeyAuth: [] + summary: Change postgresql privileges + tags: + - Database Postgresql + x-panel-log: + BeforeFunctions: [] + bodyKeys: + - database + - username + formatEN: Update [user] privileges of database [database] + formatZH: 更新数据库 [database] 用户 [username] 权限 + paramKeys: [] + /databases/pg/search: + post: + consumes: + - application/json + description: 获取 postgresql 数据库列表分页 + parameters: + - description: request + in: body + name: request + required: true + schema: + $ref: '#/definitions/dto.PostgresqlDBSearch' + responses: + "200": + description: OK + schema: + $ref: '#/definitions/dto.PageResult' + security: + - ApiKeyAuth: [] + summary: Page postgresql databases + tags: + - Database Postgresql + /databases/redis/conf: + post: + consumes: + - application/json + description: 获取 redis 配置信息 + parameters: + - description: request + in: body + name: request + required: true + schema: + $ref: '#/definitions/dto.OperationWithName' + responses: + "200": + description: OK + schema: + $ref: '#/definitions/dto.RedisConf' + security: + - ApiKeyAuth: [] + summary: Load redis conf + tags: + - Database Redis + /databases/redis/conf/update: + post: + consumes: + - application/json + description: 更新 redis 配置信息 + parameters: + - description: request + in: body + name: request + required: true + schema: + $ref: '#/definitions/dto.RedisConfUpdate' + responses: + "200": + description: OK + security: + - ApiKeyAuth: [] + summary: Update redis conf + tags: + - Database Redis + x-panel-log: + BeforeFunctions: [] + bodyKeys: [] + formatEN: update the redis database configuration information + formatZH: 更新 redis 数据库配置信息 + paramKeys: [] + /databases/redis/install/cli: + post: + description: 安装 redis cli + responses: + "200": + description: OK + security: + - ApiKeyAuth: [] + summary: Install redis-cli + tags: + - Database Redis + /databases/redis/password: + post: + consumes: + - application/json + description: 更新 redis 密码 + parameters: + - description: request + in: body + name: request + required: true + schema: + $ref: '#/definitions/dto.ChangeRedisPass' + responses: + "200": + description: OK + security: + - ApiKeyAuth: [] + summary: Change redis password + tags: + - Database Redis + x-panel-log: + BeforeFunctions: [] + bodyKeys: [] + formatEN: change the password of the redis database + formatZH: 修改 redis 数据库密码 + paramKeys: [] + /databases/redis/persistence/conf: + post: + consumes: + - application/json + description: 获取 redis 持久化配置 + parameters: + - description: request + in: body + name: request + required: true + schema: + $ref: '#/definitions/dto.OperationWithName' + responses: + "200": + description: OK + schema: + $ref: '#/definitions/dto.RedisPersistence' + security: + - ApiKeyAuth: [] + summary: Load redis persistence conf + tags: + - Database Redis + /databases/redis/persistence/update: + post: + consumes: + - application/json + description: 更新 redis 持久化配置 + parameters: + - description: request + in: body + name: request + required: true + schema: + $ref: '#/definitions/dto.RedisConfPersistenceUpdate' + responses: + "200": + description: OK + security: + - ApiKeyAuth: [] + summary: Update redis persistence conf + tags: + - Database Redis + x-panel-log: + BeforeFunctions: [] + bodyKeys: [] + formatEN: redis database persistence configuration update + formatZH: redis 数据库持久化配置更新 + paramKeys: [] + /databases/redis/status: + post: + consumes: + - application/json + description: 获取 redis 状态信息 + parameters: + - description: request + in: body + name: request + required: true + schema: + $ref: '#/definitions/dto.OperationWithName' + responses: + "200": + description: OK + schema: + $ref: '#/definitions/dto.RedisStatus' + security: + - ApiKeyAuth: [] + summary: Load redis status info + tags: + - Database Redis + /databases/remote: + post: + consumes: + - application/json + description: 获取 mysql 远程访问权限 + parameters: + - description: request + in: body + name: request + required: true + schema: + $ref: '#/definitions/dto.OperationWithNameAndType' + responses: + "200": + description: OK + schema: + type: boolean + security: + - ApiKeyAuth: [] + summary: Load mysql remote access + tags: + - Database Mysql + /databases/search: + post: + consumes: + - application/json + description: 获取 mysql 数据库列表分页 + parameters: + - description: request + in: body + name: request + required: true + schema: + $ref: '#/definitions/dto.MysqlDBSearch' + responses: + "200": + description: OK + schema: + $ref: '#/definitions/dto.PageResult' + security: + - ApiKeyAuth: [] + summary: Page mysql databases + tags: + - Database Mysql + /databases/status: + post: + consumes: + - application/json + description: 获取 mysql 状态信息 + parameters: + - description: request + in: body + name: request + required: true + schema: + $ref: '#/definitions/dto.OperationWithNameAndType' + responses: + "200": + description: OK + schema: + $ref: '#/definitions/dto.MysqlStatus' + security: + - ApiKeyAuth: [] + summary: Load mysql status info + tags: + - Database Mysql + /databases/variables: + post: + consumes: + - application/json + description: 获取 mysql 性能参数信息 + parameters: + - description: request + in: body + name: request + required: true + schema: + $ref: '#/definitions/dto.OperationWithNameAndType' + responses: + "200": + description: OK + schema: + $ref: '#/definitions/dto.MysqlVariables' + security: + - ApiKeyAuth: [] + summary: Load mysql variables info + tags: + - Database Mysql + /databases/variables/update: + post: + consumes: + - application/json + description: mysql 性能调优 + parameters: + - description: request + in: body + name: request + required: true + schema: + $ref: '#/definitions/dto.MysqlVariablesUpdate' + responses: + "200": + description: OK + security: + - ApiKeyAuth: [] + summary: Update mysql variables + tags: + - Database Mysql + x-panel-log: + BeforeFunctions: [] + bodyKeys: [] + formatEN: adjust mysql database performance parameters + formatZH: 调整 mysql 数据库性能参数 + paramKeys: [] + /db/remote/del/check: + post: + consumes: + - application/json + description: Mysql 远程数据库删除前检查 + parameters: + - description: request + in: body + name: request + required: true + schema: + $ref: '#/definitions/dto.OperateByID' + responses: + "200": + description: OK + schema: + items: + type: string + type: array + security: + - ApiKeyAuth: [] + summary: Check before delete remote database + tags: + - Database + /files: + post: + consumes: + - application/json + description: 创建文件/文件夹 + parameters: + - description: request + in: body + name: request + required: true + schema: + $ref: '#/definitions/request.FileCreate' + responses: + "200": + description: OK + security: + - ApiKeyAuth: [] + summary: Create file + tags: + - File + x-panel-log: + BeforeFunctions: [] + bodyKeys: + - path + formatEN: Create dir or file [path] + formatZH: 创建文件/文件夹 [path] + paramKeys: [] + /files/batch/del: + post: + consumes: + - application/json + description: 批量删除文件/文件夹 + parameters: + - description: request + in: body + name: request + required: true + schema: + $ref: '#/definitions/request.FileBatchDelete' + responses: + "200": + description: OK + security: + - ApiKeyAuth: [] + summary: Batch delete file + tags: + - File + x-panel-log: + BeforeFunctions: [] + bodyKeys: + - paths + formatEN: Batch delete dir or file [paths] + formatZH: 批量删除文件/文件夹 [paths] + paramKeys: [] + /files/batch/role: + post: + consumes: + - application/json + description: 批量修改文件权限和用户/组 + parameters: + - description: request + in: body + name: request + required: true + schema: + $ref: '#/definitions/request.FileRoleReq' + responses: + "200": + description: OK + security: + - ApiKeyAuth: [] + summary: Batch change file mode and owner + tags: + - File + x-panel-log: + BeforeFunctions: [] + bodyKeys: + - paths + - mode + - user + - group + formatEN: Batch change file mode and owner [paths] => [mode]/[user]/[group] + formatZH: 批量修改文件权限和用户/组 [paths] => [mode]/[user]/[group] + paramKeys: [] + /files/check: + post: + consumes: + - application/json + description: 检测文件是否存在 + parameters: + - description: request + in: body + name: request + required: true + schema: + $ref: '#/definitions/request.FilePathCheck' + responses: + "200": + description: OK + security: + - ApiKeyAuth: [] + summary: Check file exist + tags: + - File + /files/chunkdownload: + post: + consumes: + - application/json + description: 分片下载下载文件 + parameters: + - description: request + in: body + name: request + required: true + schema: + $ref: '#/definitions/request.FileDownload' + responses: + "200": + description: OK + security: + - ApiKeyAuth: [] + summary: Chunk Download file + tags: + - File + x-panel-log: + BeforeFunctions: [] + bodyKeys: + - name + formatEN: Download file [name] + formatZH: 下载文件 [name] + paramKeys: [] + /files/chunkupload: + post: + description: 分片上传文件 + parameters: + - description: request + in: formData + name: file + required: true + type: file + responses: + "200": + description: OK + security: + - ApiKeyAuth: [] + summary: ChunkUpload file + tags: + - File + /files/compress: + post: + consumes: + - application/json + description: 压缩文件 + parameters: + - description: request + in: body + name: request + required: true + schema: + $ref: '#/definitions/request.FileCompress' + responses: + "200": + description: OK + security: + - ApiKeyAuth: [] + summary: Compress file + tags: + - File + x-panel-log: + BeforeFunctions: [] + bodyKeys: + - name + formatEN: Compress file [name] + formatZH: 压缩文件 [name] + paramKeys: [] + /files/content: + post: + consumes: + - application/json + description: 获取文件内容 + parameters: + - description: request + in: body + name: request + required: true + schema: + $ref: '#/definitions/request.FileContentReq' + responses: + "200": + description: OK + schema: + $ref: '#/definitions/response.FileInfo' + security: + - ApiKeyAuth: [] + summary: Load file content + tags: + - File + x-panel-log: + BeforeFunctions: [] + bodyKeys: + - path + formatEN: Load file content [path] + formatZH: 获取文件内容 [path] + paramKeys: [] + /files/decompress: + post: + consumes: + - application/json + description: 解压文件 + parameters: + - description: request + in: body + name: request + required: true + schema: + $ref: '#/definitions/request.FileDeCompress' + responses: + "200": + description: OK + security: + - ApiKeyAuth: [] + summary: Decompress file + tags: + - File + x-panel-log: + BeforeFunctions: [] + bodyKeys: + - path + formatEN: Decompress file [path] + formatZH: 解压 [path] + paramKeys: [] + /files/del: + post: + consumes: + - application/json + description: 删除文件/文件夹 + parameters: + - description: request + in: body + name: request + required: true + schema: + $ref: '#/definitions/request.FileDelete' + responses: + "200": + description: OK + security: + - ApiKeyAuth: [] + summary: Delete file + tags: + - File + x-panel-log: + BeforeFunctions: [] + bodyKeys: + - path + formatEN: Delete dir or file [path] + formatZH: 删除文件/文件夹 [path] + paramKeys: [] + /files/download: + get: + consumes: + - application/json + description: 下载文件 + responses: + "200": + description: OK + security: + - ApiKeyAuth: [] + summary: Download file + tags: + - File + /files/favorite: + post: + consumes: + - application/json + description: 创建收藏 + parameters: + - description: request + in: body + name: request + required: true + schema: + $ref: '#/definitions/request.FavoriteCreate' + responses: + "200": + description: OK + security: + - ApiKeyAuth: [] + summary: Create favorite + tags: + - File + x-panel-log: + BeforeFunctions: [] + bodyKeys: + - path + formatEN: 收藏文件/文件夹 [path] + formatZH: 收藏文件/文件夹 [path] + paramKeys: [] + /files/favorite/del: + post: + consumes: + - application/json + description: 删除收藏 + parameters: + - description: request + in: body + name: request + required: true + schema: + $ref: '#/definitions/request.FavoriteDelete' + responses: + "200": + description: OK + security: + - ApiKeyAuth: [] + summary: Delete favorite + tags: + - File + x-panel-log: + BeforeFunctions: + - db: favorites + input_column: id + input_value: id + isList: false + output_column: path + output_value: path + bodyKeys: + - id + formatEN: delete avorite [path] + formatZH: 删除收藏 [path] + paramKeys: [] + /files/favorite/search: + post: + consumes: + - application/json + description: 获取收藏列表 + parameters: + - description: request + in: body + name: request + required: true + schema: + $ref: '#/definitions/dto.PageInfo' + responses: + "200": + description: OK + security: + - ApiKeyAuth: [] + summary: List favorites + tags: + - File + /files/mode: + post: + consumes: + - application/json + description: 修改文件权限 + parameters: + - description: request + in: body + name: request + required: true + schema: + $ref: '#/definitions/request.FileCreate' + responses: + "200": + description: OK + security: + - ApiKeyAuth: [] + summary: Change file mode + tags: + - File + x-panel-log: + BeforeFunctions: [] + bodyKeys: + - path + - mode + formatEN: Change mode [paths] => [mode] + formatZH: 修改权限 [paths] => [mode] + paramKeys: [] + /files/move: + post: + consumes: + - application/json + description: 移动文件 + parameters: + - description: request + in: body + name: request + required: true + schema: + $ref: '#/definitions/request.FileMove' + responses: + "200": + description: OK + security: + - ApiKeyAuth: [] + summary: Move file + tags: + - File + x-panel-log: + BeforeFunctions: [] + bodyKeys: + - oldPaths + - newPath + formatEN: Move [oldPaths] => [newPath] + formatZH: 移动文件 [oldPaths] => [newPath] + paramKeys: [] + /files/owner: + post: + consumes: + - application/json + description: 修改文件用户/组 + parameters: + - description: request + in: body + name: request + required: true + schema: + $ref: '#/definitions/request.FileRoleUpdate' + responses: + "200": + description: OK + security: + - ApiKeyAuth: [] + summary: Change file owner + tags: + - File + x-panel-log: + BeforeFunctions: [] + bodyKeys: + - path + - user + - group + formatEN: Change owner [paths] => [user]/[group] + formatZH: 修改用户/组 [paths] => [user]/[group] + paramKeys: [] + /files/read: + post: + description: 按行读取日志文件 + parameters: + - description: request + in: body + name: request + required: true + schema: + $ref: '#/definitions/request.FileReadByLineReq' + responses: + "200": + description: OK + security: + - ApiKeyAuth: [] + summary: Read file by Line + tags: + - File + /files/recycle/clear: + post: + consumes: + - application/json + description: 清空回收站文件 + responses: + "200": + description: OK + security: + - ApiKeyAuth: [] + summary: Clear RecycleBin files + tags: + - File + x-panel-log: + BeforeFunctions: [] + bodyKeys: [] + formatEN: 清空回收站 + formatZH: 清空回收站 + paramKeys: [] + /files/recycle/reduce: + post: + consumes: + - application/json + description: 还原回收站文件 + parameters: + - description: request + in: body + name: request + required: true + schema: + $ref: '#/definitions/request.RecycleBinReduce' + responses: + "200": + description: OK + security: + - ApiKeyAuth: [] + summary: Reduce RecycleBin files + tags: + - File + x-panel-log: + BeforeFunctions: [] + bodyKeys: + - name + formatEN: Reduce RecycleBin file [name] + formatZH: 还原回收站文件 [name] + paramKeys: [] + /files/recycle/search: + post: + consumes: + - application/json + description: 获取回收站文件列表 + parameters: + - description: request + in: body + name: request + required: true + schema: + $ref: '#/definitions/dto.PageInfo' + responses: + "200": + description: OK + security: + - ApiKeyAuth: [] + summary: List RecycleBin files + tags: + - File + /files/recycle/status: + get: + consumes: + - application/json + description: 获取回收站状态 + responses: + "200": + description: OK + security: + - ApiKeyAuth: [] + summary: Get RecycleBin status + tags: + - File + /files/rename: + post: + consumes: + - application/json + description: 修改文件名称 + parameters: + - description: request + in: body + name: request + required: true + schema: + $ref: '#/definitions/request.FileRename' + responses: + "200": + description: OK + security: + - ApiKeyAuth: [] + summary: Change file name + tags: + - File + x-panel-log: + BeforeFunctions: [] + bodyKeys: + - oldName + - newName + formatEN: Rename [oldName] => [newName] + formatZH: 重命名 [oldName] => [newName] + paramKeys: [] + /files/save: + post: + consumes: + - application/json + description: 更新文件内容 + parameters: + - description: request + in: body + name: request + required: true + schema: + $ref: '#/definitions/request.FileEdit' + responses: + "200": + description: OK + security: + - ApiKeyAuth: [] + summary: Update file content + tags: + - File + x-panel-log: + BeforeFunctions: [] + bodyKeys: + - path + formatEN: Update file content [path] + formatZH: 更新文件内容 [path] + paramKeys: [] + /files/search: + post: + consumes: + - application/json + description: 获取文件列表 + parameters: + - description: request + in: body + name: request + required: true + schema: + $ref: '#/definitions/request.FileOption' + responses: + "200": + description: OK + schema: + $ref: '#/definitions/response.FileInfo' + security: + - ApiKeyAuth: [] + summary: List files + tags: + - File + /files/size: + post: + consumes: + - application/json + description: 获取文件夹大小 + parameters: + - description: request + in: body + name: request + required: true + schema: + $ref: '#/definitions/request.DirSizeReq' + responses: + "200": + description: OK + security: + - ApiKeyAuth: [] + summary: Load file size + tags: + - File + x-panel-log: + BeforeFunctions: [] + bodyKeys: + - path + formatEN: Load file size [path] + formatZH: 获取文件夹大小 [path] + paramKeys: [] + /files/tree: + post: + consumes: + - application/json + description: 加载文件树 + parameters: + - description: request + in: body + name: request + required: true + schema: + $ref: '#/definitions/request.FileOption' + responses: + "200": + description: OK + schema: + items: + $ref: '#/definitions/response.FileTree' + type: array + security: + - ApiKeyAuth: [] + summary: Load files tree + tags: + - File + /files/upload: + post: + description: 上传文件 + parameters: + - description: request + in: formData + name: file + required: true + type: file + responses: + "200": + description: OK + security: + - ApiKeyAuth: [] + summary: Upload file + tags: + - File + x-panel-log: + BeforeFunctions: [] + bodyKeys: + - path + formatEN: Upload file [path] + formatZH: 上传文件 [path] + paramKeys: [] + /files/upload/search: + post: + consumes: + - application/json + description: 分页获取上传文件 + parameters: + - description: request + in: body + name: request + required: true + schema: + $ref: '#/definitions/request.SearchUploadWithPage' + responses: + "200": + description: OK + schema: + items: + $ref: '#/definitions/response.FileInfo' + type: array + security: + - ApiKeyAuth: [] + summary: Page file + tags: + - File + /files/wget: + post: + consumes: + - application/json + description: 下载远端文件 + parameters: + - description: request + in: body + name: request + required: true + schema: + $ref: '#/definitions/request.FileWget' + responses: + "200": + description: OK + security: + - ApiKeyAuth: [] + summary: Wget file + tags: + - File + x-panel-log: + BeforeFunctions: [] + bodyKeys: + - url + - path + - name + formatEN: Download url => [path]/[name] + formatZH: 下载 url => [path]/[name] + paramKeys: [] + /groups: + post: + consumes: + - application/json + description: 创建系统组 + parameters: + - description: request + in: body + name: request + required: true + schema: + $ref: '#/definitions/dto.GroupCreate' + responses: + "200": + description: OK + security: + - ApiKeyAuth: [] + summary: Create group + tags: + - System Group + x-panel-log: + BeforeFunctions: [] + bodyKeys: + - name + - type + formatEN: create group [name][type] + formatZH: 创建组 [name][type] + paramKeys: [] + /groups/del: + post: + consumes: + - application/json + description: 删除系统组 + parameters: + - description: request + in: body + name: request + required: true + schema: + $ref: '#/definitions/dto.OperateByID' + responses: + "200": + description: OK + security: + - ApiKeyAuth: [] + summary: Delete group + tags: + - System Group + x-panel-log: + BeforeFunctions: + - db: groups + input_column: id + input_value: id + isList: false + output_column: name + output_value: name + - db: groups + input_column: id + input_value: id + isList: false + output_column: type + output_value: type + bodyKeys: + - id + formatEN: delete group [type][name] + formatZH: 删除组 [type][name] + paramKeys: [] + /groups/search: + post: + consumes: + - application/json + description: 查询系统组 + parameters: + - description: request + in: body + name: request + required: true + schema: + $ref: '#/definitions/dto.GroupSearch' + responses: + "200": + description: OK + schema: + items: + $ref: '#/definitions/dto.GroupInfo' + type: array + security: + - ApiKeyAuth: [] + summary: List groups + tags: + - System Group + /groups/update: + post: + consumes: + - application/json + description: 更新系统组 + parameters: + - description: request + in: body + name: request + required: true + schema: + $ref: '#/definitions/dto.GroupUpdate' + responses: + "200": + description: OK + security: + - ApiKeyAuth: [] + summary: Update group + tags: + - System Group + x-panel-log: + BeforeFunctions: [] + bodyKeys: + - name + - type + formatEN: update group [name][type] + formatZH: 更新组 [name][type] + paramKeys: [] + /host/conffile/update: + post: + consumes: + - application/json + description: 上传文件更新 SSH 配置 + parameters: + - description: request + in: body + name: request + required: true + schema: + $ref: '#/definitions/dto.SSHConf' + responses: + "200": + description: OK + security: + - ApiKeyAuth: [] + summary: Update host SSH setting by file + tags: + - SSH + x-panel-log: + BeforeFunctions: [] + bodyKeys: [] + formatEN: update SSH conf + formatZH: 修改 SSH 配置文件 + paramKeys: [] + /host/ssh/conf: + get: + description: 获取 SSH 配置文件 + responses: + "200": + description: OK + security: + - ApiKeyAuth: [] + summary: Load host SSH conf + tags: + - SSH + /host/ssh/generate: + post: + consumes: + - application/json + description: 生成 SSH 密钥 + parameters: + - description: request + in: body + name: request + required: true + schema: + $ref: '#/definitions/dto.GenerateSSH' + responses: + "200": + description: OK + security: + - ApiKeyAuth: [] + summary: Generate host SSH secret + tags: + - SSH + x-panel-log: + BeforeFunctions: [] + bodyKeys: [] + formatEN: generate SSH secret + formatZH: '生成 SSH 密钥 ' + paramKeys: [] + /host/ssh/log: + post: + consumes: + - application/json + description: 获取 SSH 登录日志 + parameters: + - description: request + in: body + name: request + required: true + schema: + $ref: '#/definitions/dto.SearchSSHLog' + responses: + "200": + description: OK + schema: + $ref: '#/definitions/dto.SSHLog' + security: + - ApiKeyAuth: [] + summary: Load host SSH logs + tags: + - SSH + /host/ssh/operate: + post: + consumes: + - application/json + description: 修改 SSH 服务状态 + parameters: + - description: request + in: body + name: request + required: true + schema: + $ref: '#/definitions/dto.Operate' + responses: {} + security: + - ApiKeyAuth: [] + summary: Operate SSH + tags: + - SSH + x-panel-log: + BeforeFunctions: [] + bodyKeys: + - operation + formatEN: '[operation] SSH' + formatZH: '[operation] SSH ' + paramKeys: [] + /host/ssh/search: + post: + description: 加载 SSH 配置信息 + responses: + "200": + description: OK + schema: + $ref: '#/definitions/dto.SSHInfo' + security: + - ApiKeyAuth: [] + summary: Load host SSH setting info + tags: + - SSH + /host/ssh/secret: + post: + consumes: + - application/json + description: 获取 SSH 密钥 + parameters: + - description: request + in: body + name: request + required: true + schema: + $ref: '#/definitions/dto.GenerateLoad' + responses: + "200": + description: OK + security: + - ApiKeyAuth: [] + summary: Load host SSH secret + tags: + - SSH + /host/ssh/update: + post: + consumes: + - application/json + description: 更新 SSH 配置 + parameters: + - description: request + in: body + name: request + required: true + schema: + $ref: '#/definitions/dto.SSHUpdate' + responses: + "200": + description: OK + security: + - ApiKeyAuth: [] + summary: Update host SSH setting + tags: + - SSH + x-panel-log: + BeforeFunctions: [] + bodyKeys: + - key + - value + formatEN: update SSH setting [key] => [value] + formatZH: 修改 SSH 配置 [key] => [value] + paramKeys: [] + /host/tool: + post: + consumes: + - application/json + description: 获取主机工具状态 + parameters: + - description: request + in: body + name: request + required: true + schema: + $ref: '#/definitions/request.HostToolReq' + responses: + "200": + description: OK + security: + - ApiKeyAuth: [] + summary: Get tool + tags: + - Host tool + /host/tool/config: + post: + consumes: + - application/json + description: 操作主机工具配置文件 + parameters: + - description: request + in: body + name: request + required: true + schema: + $ref: '#/definitions/request.HostToolConfig' + responses: + "200": + description: OK + security: + - ApiKeyAuth: [] + summary: Get tool config + tags: + - Host tool + x-panel-log: + BeforeFunctions: [] + bodyKeys: + - operate + formatEN: '[operate] tool config' + formatZH: '[operate] 主机工具配置文件 ' + paramKeys: [] + /host/tool/create: + post: + consumes: + - application/json + description: 创建主机工具配置 + parameters: + - description: request + in: body + name: request + required: true + schema: + $ref: '#/definitions/request.HostToolCreate' + responses: + "200": + description: OK + security: + - ApiKeyAuth: [] + summary: Create Host tool Config + tags: + - Host tool + x-panel-log: + BeforeFunctions: [] + bodyKeys: + - type + formatEN: create [type] config + formatZH: 创建 [type] 配置 + paramKeys: [] + /host/tool/log: + post: + consumes: + - application/json + description: 获取主机工具日志 + parameters: + - description: request + in: body + name: request + required: true + schema: + $ref: '#/definitions/request.HostToolLogReq' + responses: + "200": + description: OK + security: + - ApiKeyAuth: [] + summary: Get tool + tags: + - Host tool + /host/tool/operate: + post: + consumes: + - application/json + description: 操作主机工具 + parameters: + - description: request + in: body + name: request + required: true + schema: + $ref: '#/definitions/request.HostToolReq' + responses: + "200": + description: OK + security: + - ApiKeyAuth: [] + summary: Operate tool + tags: + - Host tool + x-panel-log: + BeforeFunctions: [] + bodyKeys: + - operate + - type + formatEN: '[operate] [type]' + formatZH: '[operate] [type] ' + paramKeys: [] + /host/tool/supervisor/process: + get: + consumes: + - application/json + description: 获取 Supervisor 进程配置 + responses: + "200": + description: OK + security: + - ApiKeyAuth: [] + summary: Get Supervisor process config + tags: + - Host tool + post: + consumes: + - application/json + description: 操作守护进程 + parameters: + - description: request + in: body + name: request + required: true + schema: + $ref: '#/definitions/request.SupervisorProcessConfig' + responses: + "200": + description: OK + security: + - ApiKeyAuth: [] + summary: Create Supervisor process + tags: + - Host tool + x-panel-log: + BeforeFunctions: [] + bodyKeys: + - operate + formatEN: '[operate] process' + formatZH: '[operate] 守护进程 ' + paramKeys: [] + /host/tool/supervisor/process/file: + post: + consumes: + - application/json + description: 操作 Supervisor 进程文件 + parameters: + - description: request + in: body + name: request + required: true + schema: + $ref: '#/definitions/request.SupervisorProcessFileReq' + responses: + "200": + description: OK + security: + - ApiKeyAuth: [] + summary: Get Supervisor process config + tags: + - Host tool + x-panel-log: + BeforeFunctions: [] + bodyKeys: + - operate + formatEN: '[operate] Supervisor Process Config file' + formatZH: '[operate] Supervisor 进程文件 ' + paramKeys: [] + /hosts: + post: + consumes: + - application/json + description: 创建主机 + parameters: + - description: request + in: body + name: request + required: true + schema: + $ref: '#/definitions/dto.HostOperate' + responses: + "200": + description: OK + security: + - ApiKeyAuth: [] + summary: Create host + tags: + - Host + x-panel-log: + BeforeFunctions: [] + bodyKeys: + - name + - addr + formatEN: create host [name][addr] + formatZH: 创建主机 [name][addr] + paramKeys: [] + /hosts/command: + get: + description: 获取快速命令列表 + responses: + "200": + description: OK + schema: + $ref: '#/definitions/dto.CommandInfo' + security: + - ApiKeyAuth: [] + summary: List commands + tags: + - Command + post: + consumes: + - application/json + description: 创建快速命令 + parameters: + - description: request + in: body + name: request + required: true + schema: + $ref: '#/definitions/dto.CommandOperate' + responses: + "200": + description: OK + security: + - ApiKeyAuth: [] + summary: Create command + tags: + - Command + x-panel-log: + BeforeFunctions: [] + bodyKeys: + - name + - command + formatEN: create quick command [name][command] + formatZH: 创建快捷命令 [name][command] + paramKeys: [] + /hosts/command/del: + post: + consumes: + - application/json + description: 删除快速命令 + parameters: + - description: request + in: body + name: request + required: true + schema: + $ref: '#/definitions/dto.BatchDeleteReq' + responses: + "200": + description: OK + security: + - ApiKeyAuth: [] + summary: Delete command + tags: + - Command + x-panel-log: + BeforeFunctions: + - db: commands + input_column: id + input_value: ids + isList: true + output_column: name + output_value: names + bodyKeys: + - ids + formatEN: delete quick command [names] + formatZH: 删除快捷命令 [names] + paramKeys: [] + /hosts/command/redis: + get: + description: 获取 redis 快速命令列表 + responses: + "200": + description: OK + schema: + type: Array + security: + - ApiKeyAuth: [] + summary: List redis commands + tags: + - Redis Command + post: + consumes: + - application/json + description: 保存 Redis 快速命令 + parameters: + - description: request + in: body + name: request + required: true + schema: + $ref: '#/definitions/dto.RedisCommand' + responses: + "200": + description: OK + security: + - ApiKeyAuth: [] + summary: Save redis command + tags: + - Redis Command + x-panel-log: + BeforeFunctions: [] + bodyKeys: + - name + - command + formatEN: save quick command for redis [name][command] + formatZH: 保存 redis 快捷命令 [name][command] + paramKeys: [] + /hosts/command/redis/del: + post: + consumes: + - application/json + description: 删除 redis 快速命令 + parameters: + - description: request + in: body + name: request + required: true + schema: + $ref: '#/definitions/dto.BatchDeleteReq' + responses: + "200": + description: OK + security: + - ApiKeyAuth: [] + summary: Delete redis command + tags: + - Redis Command + x-panel-log: + BeforeFunctions: + - db: redis_commands + input_column: id + input_value: ids + isList: true + output_column: name + output_value: names + bodyKeys: + - ids + formatEN: delete quick command of redis [names] + formatZH: 删除 redis 快捷命令 [names] + paramKeys: [] + /hosts/command/redis/search: + post: + consumes: + - application/json + description: 获取 redis 快速命令列表分页 + parameters: + - description: request + in: body + name: request + required: true + schema: + $ref: '#/definitions/dto.SearchWithPage' + responses: + "200": + description: OK + schema: + $ref: '#/definitions/dto.PageResult' + security: + - ApiKeyAuth: [] + summary: Page redis commands + tags: + - Redis Command + /hosts/command/search: + post: + consumes: + - application/json + description: 获取快速命令列表分页 + parameters: + - description: request + in: body + name: request + required: true + schema: + $ref: '#/definitions/dto.SearchWithPage' + responses: + "200": + description: OK + schema: + $ref: '#/definitions/dto.PageResult' + security: + - ApiKeyAuth: [] + summary: Page commands + tags: + - Command + /hosts/command/tree: + get: + consumes: + - application/json + description: 获取快速命令树 + responses: + "200": + description: OK + schema: + type: Array + security: + - ApiKeyAuth: [] + summary: Tree commands + tags: + - Command + /hosts/command/update: + post: + consumes: + - application/json + description: 更新快速命令 + parameters: + - description: request + in: body + name: request + required: true + schema: + $ref: '#/definitions/dto.CommandOperate' + responses: + "200": + description: OK + security: + - ApiKeyAuth: [] + summary: Update command + tags: + - Command + x-panel-log: + BeforeFunctions: [] + bodyKeys: + - name + formatEN: update quick command [name] + formatZH: 更新快捷命令 [name] + paramKeys: [] + /hosts/del: + post: + consumes: + - application/json + description: 删除主机 + parameters: + - description: request + in: body + name: request + required: true + schema: + $ref: '#/definitions/dto.BatchDeleteReq' + responses: + "200": + description: OK + security: + - ApiKeyAuth: [] + summary: Delete host + tags: + - Host + x-panel-log: + BeforeFunctions: + - db: hosts + input_column: id + input_value: ids + isList: true + output_column: addr + output_value: addrs + bodyKeys: + - ids + formatEN: delete host [addrs] + formatZH: 删除主机 [addrs] + paramKeys: [] + /hosts/firewall/base: + get: + description: 获取防火墙基础信息 + responses: + "200": + description: OK + schema: + $ref: '#/definitions/dto.FirewallBaseInfo' + security: + - ApiKeyAuth: [] + summary: Load firewall base info + tags: + - Firewall + /hosts/firewall/batch: + post: + consumes: + - application/json + description: 批量删除防火墙规则 + parameters: + - description: request + in: body + name: request + required: true + schema: + $ref: '#/definitions/dto.BatchRuleOperate' + responses: + "200": + description: OK + security: + - ApiKeyAuth: [] + summary: Create group + tags: + - Firewall + /hosts/firewall/forward: + post: + consumes: + - application/json + description: 更新防火墙端口转发规则 + parameters: + - description: request + in: body + name: request + required: true + schema: + $ref: '#/definitions/dto.ForwardRuleOperate' + responses: + "200": + description: OK + security: + - ApiKeyAuth: [] + summary: Create group + tags: + - Firewall + x-panel-log: + BeforeFunctions: [] + bodyKeys: + - source_port + formatEN: update port forward rules [source_port] + formatZH: 更新端口转发规则 [source_port] + paramKeys: [] + /hosts/firewall/ip: + post: + consumes: + - application/json + description: 创建防火墙 IP 规则 + parameters: + - description: request + in: body + name: request + required: true + schema: + $ref: '#/definitions/dto.AddrRuleOperate' + responses: + "200": + description: OK + security: + - ApiKeyAuth: [] + summary: Create group + tags: + - Firewall + x-panel-log: + BeforeFunctions: [] + bodyKeys: + - strategy + - address + formatEN: create address rules [strategy][address] + formatZH: 添加 ip 规则 [strategy] [address] + paramKeys: [] + /hosts/firewall/operate: + post: + consumes: + - application/json + description: 修改防火墙状态 + parameters: + - description: request + in: body + name: request + required: true + schema: + $ref: '#/definitions/dto.FirewallOperation' + responses: + "200": + description: OK + schema: + $ref: '#/definitions/dto.PageResult' + security: + - ApiKeyAuth: [] + summary: Page firewall status + tags: + - Firewall + x-panel-log: + BeforeFunctions: [] + bodyKeys: + - operation + formatEN: '[operation] firewall' + formatZH: '[operation] 防火墙' + paramKeys: [] + /hosts/firewall/port: + post: + consumes: + - application/json + description: 创建防火墙端口规则 + parameters: + - description: request + in: body + name: request + required: true + schema: + $ref: '#/definitions/dto.PortRuleOperate' + responses: + "200": + description: OK + security: + - ApiKeyAuth: [] + summary: Create group + tags: + - Firewall + x-panel-log: + BeforeFunctions: [] + bodyKeys: + - port + - strategy + formatEN: create port rules [strategy][port] + formatZH: 添加端口规则 [strategy] [port] + paramKeys: [] + /hosts/firewall/search: + post: + consumes: + - application/json + description: 获取防火墙规则列表分页 + parameters: + - description: request + in: body + name: request + required: true + schema: + $ref: '#/definitions/dto.RuleSearch' + responses: + "200": + description: OK + schema: + $ref: '#/definitions/dto.PageResult' + security: + - ApiKeyAuth: [] + summary: Page firewall rules + tags: + - Firewall + /hosts/firewall/update/addr: + post: + consumes: + - application/json + description: 更新 ip 防火墙规则 + parameters: + - description: request + in: body + name: request + required: true + schema: + $ref: '#/definitions/dto.AddrRuleUpdate' + responses: + "200": + description: OK + security: + - ApiKeyAuth: [] + summary: Create group + tags: + - Firewall + /hosts/firewall/update/description: + post: + consumes: + - application/json + description: 更新防火墙描述 + parameters: + - description: request + in: body + name: request + required: true + schema: + $ref: '#/definitions/dto.UpdateFirewallDescription' + responses: + "200": + description: OK + security: + - ApiKeyAuth: [] + summary: Update rule description + tags: + - Firewall + /hosts/firewall/update/port: + post: + consumes: + - application/json + description: 更新端口防火墙规则 + parameters: + - description: request + in: body + name: request + required: true + schema: + $ref: '#/definitions/dto.PortRuleUpdate' + responses: + "200": + description: OK + security: + - ApiKeyAuth: [] + summary: Create group + tags: + - Firewall + /hosts/monitor/clean: + post: + description: 清空监控数据 + responses: + "200": + description: OK + security: + - ApiKeyAuth: [] + summary: Clean monitor datas + tags: + - Monitor + x-panel-log: + BeforeFunctions: [] + bodyKeys: [] + formatEN: clean monitor datas + formatZH: 清空监控数据 + paramKeys: [] + /hosts/monitor/search: + post: + description: 获取监控数据 + parameters: + - description: request + in: body + name: request + required: true + schema: + $ref: '#/definitions/dto.MonitorSearch' + responses: + "200": + description: OK + security: + - ApiKeyAuth: [] + summary: Load monitor datas + tags: + - Monitor + /hosts/search: + post: + consumes: + - application/json + description: 获取主机列表分页 + parameters: + - description: request + in: body + name: request + required: true + schema: + $ref: '#/definitions/dto.SearchHostWithPage' + responses: + "200": + description: OK + schema: + items: + $ref: '#/definitions/dto.HostTree' + type: array + security: + - ApiKeyAuth: [] + summary: Page host + tags: + - Host + /hosts/test/byid/:id: + post: + consumes: + - application/json + description: 测试主机连接 + parameters: + - description: request + in: path + name: id + required: true + type: integer + responses: + "200": + description: OK + schema: + type: boolean + security: + - ApiKeyAuth: [] + summary: Test host conn by host id + tags: + - Host + /hosts/test/byinfo: + post: + consumes: + - application/json + description: 测试主机连接 + parameters: + - description: request + in: body + name: request + required: true + schema: + $ref: '#/definitions/dto.HostConnTest' + responses: + "200": + description: OK + security: + - ApiKeyAuth: [] + summary: Test host conn by info + tags: + - Host + /hosts/tree: + post: + consumes: + - application/json + description: 加载主机树 + parameters: + - description: request + in: body + name: request + required: true + schema: + $ref: '#/definitions/dto.SearchForTree' + responses: + "200": + description: OK + schema: + items: + $ref: '#/definitions/dto.HostTree' + type: array + security: + - ApiKeyAuth: [] + summary: Load host tree + tags: + - Host + /hosts/update: + post: + consumes: + - application/json + description: 更新主机 + parameters: + - description: request + in: body + name: request + required: true + schema: + $ref: '#/definitions/dto.HostOperate' + responses: + "200": + description: OK + security: + - ApiKeyAuth: [] + summary: Update host + tags: + - Host + x-panel-log: + BeforeFunctions: [] + bodyKeys: + - name + - addr + formatEN: update host [name][addr] + formatZH: 更新主机信息 [name][addr] + paramKeys: [] + /hosts/update/group: + post: + consumes: + - application/json + description: 切换分组 + parameters: + - description: request + in: body + name: request + required: true + schema: + $ref: '#/definitions/dto.ChangeHostGroup' + responses: + "200": + description: OK + security: + - ApiKeyAuth: [] + summary: Update host group + tags: + - Host + x-panel-log: + BeforeFunctions: + - db: hosts + input_column: id + input_value: id + isList: false + output_column: addr + output_value: addr + bodyKeys: + - id + - group + formatEN: change host [addr] group => [group] + formatZH: 切换主机[addr]分组 => [group] + paramKeys: [] + /logs/clean: + post: + consumes: + - application/json + description: 清空操作日志 + parameters: + - description: request + in: body + name: request + required: true + schema: + $ref: '#/definitions/dto.CleanLog' + responses: + "200": + description: OK + schema: + $ref: '#/definitions/dto.PageResult' + security: + - ApiKeyAuth: [] + summary: Clean operation logs + tags: + - Logs + x-panel-log: + BeforeFunctions: [] + bodyKeys: + - logType + formatEN: Clean the [logType] log information + formatZH: 清空 [logType] 日志信息 + paramKeys: [] + /logs/login: + post: + consumes: + - application/json + description: 获取系统登录日志列表分页 + parameters: + - description: request + in: body + name: request + required: true + schema: + $ref: '#/definitions/dto.SearchLgLogWithPage' + responses: + "200": + description: OK + schema: + $ref: '#/definitions/dto.PageResult' + security: + - ApiKeyAuth: [] + summary: Page login logs + tags: + - Logs + /logs/operation: + post: + consumes: + - application/json + description: 获取系统操作日志列表分页 + parameters: + - description: request + in: body + name: request + required: true + schema: + $ref: '#/definitions/dto.SearchOpLogWithPage' + responses: + "200": + description: OK + schema: + $ref: '#/definitions/dto.PageResult' + security: + - ApiKeyAuth: [] + summary: Page operation logs + tags: + - Logs + /logs/system: + post: + description: 获取系统日志 + responses: + "200": + description: OK + security: + - ApiKeyAuth: [] + summary: Load system logs + tags: + - Logs + /logs/system/files: + get: + description: 获取系统日志文件列表 + responses: + "200": + description: OK + security: + - ApiKeyAuth: [] + summary: Load system log files + tags: + - Logs + /openresty: + get: + description: 获取 OpenResty 配置信息 + responses: + "200": + description: OK + schema: + $ref: '#/definitions/response.FileInfo' + security: + - ApiKeyAuth: [] + summary: Load OpenResty conf + tags: + - OpenResty + /openresty/clear: + post: + description: 清理 OpenResty 代理缓存 + responses: + "200": + description: OK + security: + - ApiKeyAuth: [] + summary: Clear OpenResty proxy cache + tags: + - OpenResty + x-panel-log: + BeforeFunctions: [] + bodyKeys: [] + formatEN: Clear nginx proxy cache + formatZH: 清理 Openresty 代理缓存 + paramKeys: [] + /openresty/file: + post: + consumes: + - application/json + description: 上传更新 OpenResty 配置文件 + parameters: + - description: request + in: body + name: request + required: true + schema: + $ref: '#/definitions/request.NginxConfigFileUpdate' + responses: + "200": + description: OK + security: + - ApiKeyAuth: [] + summary: Update OpenResty conf by upload file + tags: + - OpenResty + x-panel-log: + BeforeFunctions: [] + bodyKeys: [] + formatEN: Update nginx conf + formatZH: 更新 nginx 配置 + paramKeys: [] + /openresty/scope: + post: + consumes: + - application/json + description: 获取部分 OpenResty 配置信息 + parameters: + - description: request + in: body + name: request + required: true + schema: + $ref: '#/definitions/request.NginxScopeReq' + responses: + "200": + description: OK + schema: + items: + $ref: '#/definitions/response.NginxParam' + type: array + security: + - ApiKeyAuth: [] + summary: Load partial OpenResty conf + tags: + - OpenResty + /openresty/status: + get: + description: 获取 OpenResty 状态信息 + responses: + "200": + description: OK + schema: + $ref: '#/definitions/response.NginxStatus' + security: + - ApiKeyAuth: [] + summary: Load OpenResty status info + tags: + - OpenResty + /openresty/update: + post: + consumes: + - application/json + description: 更新 OpenResty 配置信息 + parameters: + - description: request + in: body + name: request + required: true + schema: + $ref: '#/definitions/request.NginxConfigUpdate' + responses: + "200": + description: OK + security: + - ApiKeyAuth: [] + summary: Update OpenResty conf + tags: + - OpenResty + x-panel-log: + BeforeFunctions: + - db: websites + input_column: id + input_value: websiteId + isList: false + output_column: primary_domain + output_value: domain + bodyKeys: + - websiteId + formatEN: Update nginx conf [domain] + formatZH: 更新 nginx 配置 [domain] + paramKeys: [] + /process/stop: + post: + description: 停止进程 + parameters: + - description: request + in: body + name: request + required: true + schema: + $ref: '#/definitions/request.ProcessReq' + responses: + "200": + description: OK + security: + - ApiKeyAuth: [] + summary: Stop Process + tags: + - Process + x-panel-log: + BeforeFunctions: [] + bodyKeys: + - PID + formatEN: 结束进程 [PID] + formatZH: 结束进程 [PID] + paramKeys: [] + /runtimes: + post: + consumes: + - application/json + description: 创建运行环境 + parameters: + - description: request + in: body + name: request + required: true + schema: + $ref: '#/definitions/request.RuntimeCreate' + responses: + "200": + description: OK + security: + - ApiKeyAuth: [] + summary: Create runtime + tags: + - Runtime + x-panel-log: + BeforeFunctions: [] + bodyKeys: + - name + formatEN: Create runtime [name] + formatZH: 创建运行环境 [name] + paramKeys: [] + /runtimes/:id: + get: + consumes: + - application/json + description: 获取运行环境 + parameters: + - description: request + in: path + name: id + required: true + type: string + responses: + "200": + description: OK + security: + - ApiKeyAuth: [] + summary: Get runtime + tags: + - Runtime + /runtimes/del: + post: + consumes: + - application/json + description: 删除运行环境 + parameters: + - description: request + in: body + name: request + required: true + schema: + $ref: '#/definitions/request.RuntimeDelete' + responses: + "200": + description: OK + security: + - ApiKeyAuth: [] + summary: Delete runtime + tags: + - Website + x-panel-log: + BeforeFunctions: [] + bodyKeys: + - id + formatEN: Delete website [name] + formatZH: 删除网站 [name] + paramKeys: [] + /runtimes/node/modules: + post: + consumes: + - application/json + description: 获取 Node 项目的 modules + parameters: + - description: request + in: body + name: request + required: true + schema: + $ref: '#/definitions/request.NodeModuleReq' + responses: + "200": + description: OK + security: + - ApiKeyAuth: [] + summary: Get Node modules + tags: + - Runtime + /runtimes/node/modules/operate: + post: + consumes: + - application/json + description: 操作 Node 项目 modules + parameters: + - description: request + in: body + name: request + required: true + schema: + $ref: '#/definitions/request.NodeModuleReq' + responses: + "200": + description: OK + security: + - ApiKeyAuth: [] + summary: Operate Node modules + tags: + - Runtime + /runtimes/node/package: + post: + consumes: + - application/json + description: 获取 Node 项目的 scripts + parameters: + - description: request + in: body + name: request + required: true + schema: + $ref: '#/definitions/request.NodePackageReq' + responses: + "200": + description: OK + security: + - ApiKeyAuth: [] + summary: Get Node package scripts + tags: + - Runtime + /runtimes/operate: + post: + consumes: + - application/json + description: 操作运行环境 + parameters: + - description: request + in: body + name: request + required: true + schema: + $ref: '#/definitions/request.RuntimeOperate' + responses: + "200": + description: OK + security: + - ApiKeyAuth: [] + summary: Operate runtime + tags: + - Runtime + x-panel-log: + BeforeFunctions: [] + bodyKeys: + - id + formatEN: Operate runtime [name] + formatZH: 操作运行环境 [name] + paramKeys: [] + /runtimes/php/extensions: + post: + consumes: + - application/json + description: Create Extensions + parameters: + - description: request + in: body + name: request + required: true + schema: + $ref: '#/definitions/request.PHPExtensionsCreate' + responses: + "200": + description: OK + security: + - ApiKeyAuth: [] + summary: Create Extensions + tags: + - PHP Extensions + /runtimes/php/extensions/del: + post: + consumes: + - application/json + description: Delete Extensions + parameters: + - description: request + in: body + name: request + required: true + schema: + $ref: '#/definitions/request.PHPExtensionsDelete' + responses: + "200": + description: OK + security: + - ApiKeyAuth: [] + summary: Delete Extensions + tags: + - PHP Extensions + /runtimes/php/extensions/search: + post: + consumes: + - application/json + description: Page Extensions + parameters: + - description: request + in: body + name: request + required: true + schema: + $ref: '#/definitions/request.PHPExtensionsSearch' + responses: + "200": + description: OK + schema: + items: + $ref: '#/definitions/response.PHPExtensionsDTO' + type: array + security: + - ApiKeyAuth: [] + summary: Page Extensions + tags: + - PHP Extensions + /runtimes/php/extensions/update: + post: + consumes: + - application/json + description: Update Extensions + parameters: + - description: request + in: body + name: request + required: true + schema: + $ref: '#/definitions/request.PHPExtensionsUpdate' + responses: + "200": + description: OK + security: + - ApiKeyAuth: [] + summary: Update Extensions + tags: + - PHP Extensions + /runtimes/search: + post: + consumes: + - application/json + description: 获取运行环境列表 + parameters: + - description: request + in: body + name: request + required: true + schema: + $ref: '#/definitions/request.RuntimeSearch' + responses: + "200": + description: OK + security: + - ApiKeyAuth: [] + summary: List runtimes + tags: + - Runtime + /runtimes/sync: + post: + consumes: + - application/json + description: 同步运行环境状态 + responses: + "200": + description: OK + security: + - ApiKeyAuth: [] + summary: Sync runtime status + tags: + - Runtime + /runtimes/update: + post: + consumes: + - application/json + description: 更新运行环境 + parameters: + - description: request + in: body + name: request + required: true + schema: + $ref: '#/definitions/request.RuntimeUpdate' + responses: + "200": + description: OK + security: + - ApiKeyAuth: [] + summary: Update runtime + tags: + - Runtime + x-panel-log: + BeforeFunctions: [] + bodyKeys: + - name + formatEN: Update runtime [name] + formatZH: 更新运行环境 [name] + paramKeys: [] + /settings/backup: + post: + consumes: + - application/json + description: 创建备份账号 + parameters: + - description: request + in: body + name: request + required: true + schema: + $ref: '#/definitions/dto.BackupOperate' + responses: + "200": + description: OK + security: + - ApiKeyAuth: [] + summary: Create backup account + tags: + - Backup Account + x-panel-log: + BeforeFunctions: [] + bodyKeys: + - type + formatEN: create backup account [type] + formatZH: 创建备份账号 [type] + paramKeys: [] + /settings/backup/backup: + post: + consumes: + - application/json + description: 备份系统数据 + parameters: + - description: request + in: body + name: request + required: true + schema: + $ref: '#/definitions/dto.CommonBackup' + responses: + "200": + description: OK + security: + - ApiKeyAuth: [] + summary: Backup system data + tags: + - Backup Account + x-panel-log: + BeforeFunctions: [] + bodyKeys: + - type + - name + - detailName + formatEN: backup [type] data [name][detailName] + formatZH: 备份 [type] 数据 [name][detailName] + paramKeys: [] + /settings/backup/del: + post: + consumes: + - application/json + description: 删除备份账号 + parameters: + - description: request + in: body + name: request + required: true + schema: + $ref: '#/definitions/dto.OperateByID' + responses: + "200": + description: OK + security: + - ApiKeyAuth: [] + summary: Delete backup account + tags: + - Backup Account + x-panel-log: + BeforeFunctions: + - db: backup_accounts + input_column: id + input_value: id + isList: false + output_column: type + output_value: types + bodyKeys: + - id + formatEN: delete backup account [types] + formatZH: 删除备份账号 [types] + paramKeys: [] + /settings/backup/onedrive: + get: + consumes: + - application/json + description: 获取 OneDrive 信息 + responses: + "200": + description: OK + schema: + $ref: '#/definitions/dto.OneDriveInfo' + security: + - ApiKeyAuth: [] + summary: Load OneDrive info + tags: + - Backup Account + /settings/backup/record/del: + post: + consumes: + - application/json + description: 删除备份记录 + parameters: + - description: request + in: body + name: request + required: true + schema: + $ref: '#/definitions/dto.BatchDeleteReq' + responses: + "200": + description: OK + security: + - ApiKeyAuth: [] + summary: Delete backup record + tags: + - Backup Account + x-panel-log: + BeforeFunctions: + - db: backup_records + input_column: id + input_value: ids + isList: true + output_column: file_name + output_value: files + bodyKeys: + - ids + formatEN: delete backup records [files] + formatZH: 删除备份记录 [files] + paramKeys: [] + /settings/backup/record/download: + post: + consumes: + - application/json + description: 下载备份记录 + parameters: + - description: request + in: body + name: request + required: true + schema: + $ref: '#/definitions/dto.DownloadRecord' + responses: + "200": + description: OK + security: + - ApiKeyAuth: [] + summary: Download backup record + tags: + - Backup Account + x-panel-log: + BeforeFunctions: [] + bodyKeys: + - source + - fileName + formatEN: download backup records [source][fileName] + formatZH: 下载备份记录 [source][fileName] + paramKeys: [] + /settings/backup/record/search: + post: + consumes: + - application/json + description: 获取备份记录列表分页 + parameters: + - description: request + in: body + name: request + required: true + schema: + $ref: '#/definitions/dto.RecordSearch' + responses: + "200": + description: OK + security: + - ApiKeyAuth: [] + summary: Page backup records + tags: + - Backup Account + /settings/backup/record/search/bycronjob: + post: + consumes: + - application/json + description: 通过计划任务获取备份记录列表分页 + parameters: + - description: request + in: body + name: request + required: true + schema: + $ref: '#/definitions/dto.RecordSearchByCronjob' + responses: + "200": + description: OK + security: + - ApiKeyAuth: [] + summary: Page backup records by cronjob + tags: + - Backup Account + /settings/backup/recover: + post: + consumes: + - application/json + description: 恢复系统数据 + parameters: + - description: request + in: body + name: request + required: true + schema: + $ref: '#/definitions/dto.CommonRecover' + responses: + "200": + description: OK + security: + - ApiKeyAuth: [] + summary: Recover system data + tags: + - Backup Account + x-panel-log: + BeforeFunctions: [] + bodyKeys: + - type + - name + - detailName + - file + formatEN: recover [type] data [name][detailName] from [file] + formatZH: 从 [file] 恢复 [type] 数据 [name][detailName] + paramKeys: [] + /settings/backup/recover/byupload: + post: + consumes: + - application/json + description: 从上传恢复系统数据 + parameters: + - description: request + in: body + name: request + required: true + schema: + $ref: '#/definitions/dto.CommonRecover' + responses: + "200": + description: OK + security: + - ApiKeyAuth: [] + summary: Recover system data by upload + tags: + - Backup Account + x-panel-log: + BeforeFunctions: [] + bodyKeys: + - type + - name + - detailName + - file + formatEN: recover [type] data [name][detailName] from [file] + formatZH: 从 [file] 恢复 [type] 数据 [name][detailName] + paramKeys: [] + /settings/backup/refresh/onedrive: + post: + description: 刷新 OneDrive token + responses: + "200": + description: OK + security: + - ApiKeyAuth: [] + summary: Refresh OneDrive token + tags: + - Backup Account + /settings/backup/search: + get: + description: 获取备份账号列表 + responses: + "200": + description: OK + schema: + items: + $ref: '#/definitions/dto.BackupInfo' + type: array + security: + - ApiKeyAuth: [] + summary: List backup accounts + tags: + - Backup Account + post: + consumes: + - application/json + description: 获取 bucket 列表 + parameters: + - description: request + in: body + name: request + required: true + schema: + $ref: '#/definitions/dto.ForBuckets' + responses: + "200": + description: OK + schema: + items: + type: string + type: array + security: + - ApiKeyAuth: [] + summary: List buckets + tags: + - Backup Account + /settings/backup/search/files: + post: + consumes: + - application/json + description: 获取备份账号内文件列表 + parameters: + - description: request + in: body + name: request + required: true + schema: + $ref: '#/definitions/dto.BackupSearchFile' + responses: + "200": + description: OK + schema: + items: + type: string + type: array + security: + - ApiKeyAuth: [] + summary: List files from backup accounts + tags: + - Backup Account + /settings/backup/update: + post: + consumes: + - application/json + description: 更新备份账号信息 + parameters: + - description: request + in: body + name: request + required: true + schema: + $ref: '#/definitions/dto.BackupOperate' + responses: + "200": + description: OK + security: + - ApiKeyAuth: [] + summary: Update backup account + tags: + - Backup Account + x-panel-log: + BeforeFunctions: [] + bodyKeys: + - type + formatEN: update backup account [types] + formatZH: 更新备份账号 [types] + paramKeys: [] + /settings/basedir: + get: + description: 获取安装根目录 + responses: + "200": + description: OK + schema: + type: string + security: + - ApiKeyAuth: [] + summary: Load local backup dir + tags: + - System Setting + /settings/bind/update: + post: + consumes: + - application/json + description: 更新系统监听信息 + parameters: + - description: request + in: body + name: request + required: true + schema: + $ref: '#/definitions/dto.BindInfo' + responses: + "200": + description: OK + security: + - ApiKeyAuth: [] + summary: Update system bind info + tags: + - System Setting + x-panel-log: + BeforeFunctions: [] + bodyKeys: + - ipv6 + - bindAddress + formatEN: 'update system bind info => ipv6: [ipv6], 监听 IP: [bindAddress]' + formatZH: '修改系统监听信息 => ipv6: [ipv6], 监听 IP: [bindAddress]' + paramKeys: [] + /settings/expired/handle: + post: + consumes: + - application/json + description: 重置过期系统登录密码 + parameters: + - description: request + in: body + name: request + required: true + schema: + $ref: '#/definitions/dto.PasswordUpdate' + responses: + "200": + description: OK + security: + - ApiKeyAuth: [] + summary: Reset system password expired + tags: + - System Setting + x-panel-log: + BeforeFunctions: [] + bodyKeys: [] + formatEN: reset an expired Password + formatZH: 重置过期密码 + paramKeys: [] + /settings/interface: + get: + consumes: + - application/json + description: 获取系统地址信息 + responses: + "200": + description: OK + security: + - ApiKeyAuth: [] + summary: Load system address + tags: + - System Setting + /settings/menu/update: + post: + consumes: + - application/json + description: 隐藏高级功能菜单 + parameters: + - description: request + in: body + name: request + required: true + schema: + $ref: '#/definitions/dto.SettingUpdate' + responses: + "200": + description: OK + security: + - ApiKeyAuth: [] + summary: Update system setting + tags: + - System Setting + x-panel-log: + BeforeFunctions: [] + bodyKeys: [] + formatEN: Hide advanced feature menu. + formatZH: 隐藏高级功能菜单 + paramKeys: [] + /settings/mfa: + post: + consumes: + - application/json + description: 获取 mfa 信息 + parameters: + - description: request + in: body + name: request + required: true + schema: + $ref: '#/definitions/dto.MfaCredential' + responses: + "200": + description: OK + schema: + $ref: '#/definitions/mfa.Otp' + security: + - ApiKeyAuth: [] + summary: Load mfa info + tags: + - System Setting + /settings/mfa/bind: + post: + consumes: + - application/json + description: Mfa 绑定 + parameters: + - description: request + in: body + name: request + required: true + schema: + $ref: '#/definitions/dto.MfaCredential' + responses: + "200": + description: OK + security: + - ApiKeyAuth: [] + summary: Bind mfa + tags: + - System Setting + x-panel-log: + BeforeFunctions: [] + bodyKeys: [] + formatEN: bind mfa + formatZH: mfa 绑定 + paramKeys: [] + /settings/password/update: + post: + consumes: + - application/json + description: 更新系统登录密码 + parameters: + - description: request + in: body + name: request + required: true + schema: + $ref: '#/definitions/dto.PasswordUpdate' + responses: + "200": + description: OK + security: + - ApiKeyAuth: [] + summary: Update system password + tags: + - System Setting + x-panel-log: + BeforeFunctions: [] + bodyKeys: [] + formatEN: update system password + formatZH: 修改系统密码 + paramKeys: [] + /settings/port/update: + post: + consumes: + - application/json + description: 更新系统端口 + parameters: + - description: request + in: body + name: request + required: true + schema: + $ref: '#/definitions/dto.PortUpdate' + responses: + "200": + description: OK + security: + - ApiKeyAuth: [] + summary: Update system port + tags: + - System Setting + x-panel-log: + BeforeFunctions: [] + bodyKeys: + - serverPort + formatEN: update system port => [serverPort] + formatZH: 修改系统端口 => [serverPort] + paramKeys: [] + /settings/proxy/update: + post: + consumes: + - application/json + description: 服务器代理配置 + parameters: + - description: request + in: body + name: request + required: true + schema: + $ref: '#/definitions/dto.ProxyUpdate' + responses: + "200": + description: OK + security: + - ApiKeyAuth: [] + summary: Update proxy setting + tags: + - System Setting + x-panel-log: + BeforeFunctions: [] + bodyKeys: + - proxyUrl + - proxyPort + formatEN: set proxy [proxyPort]:[proxyPort]. + formatZH: 服务器代理配置 [proxyPort]:[proxyPort] + paramKeys: [] + /settings/search: + post: + description: 加载系统配置信息 + responses: + "200": + description: OK + schema: + $ref: '#/definitions/dto.SettingInfo' + security: + - ApiKeyAuth: [] + summary: Load system setting info + tags: + - System Setting + /settings/search/available: + get: + description: 获取系统可用状态 + responses: + "200": + description: OK + security: + - ApiKeyAuth: [] + summary: Load system available status + tags: + - System Setting + /settings/snapshot: + post: + consumes: + - application/json + description: 创建系统快照 + parameters: + - description: request + in: body + name: request + required: true + schema: + $ref: '#/definitions/dto.SnapshotCreate' + responses: + "200": + description: OK + security: + - ApiKeyAuth: [] + summary: Create system snapshot + tags: + - System Setting + x-panel-log: + BeforeFunctions: [] + bodyKeys: + - from + - description + formatEN: Create system backup [description] to [from] + formatZH: 创建系统快照 [description] 到 [from] + paramKeys: [] + /settings/snapshot/del: + post: + consumes: + - application/json + description: 删除系统快照 + parameters: + - description: request + in: body + name: request + required: true + schema: + $ref: '#/definitions/dto.SnapshotBatchDelete' + responses: + "200": + description: OK + security: + - ApiKeyAuth: [] + summary: Delete system backup + tags: + - System Setting + x-panel-log: + BeforeFunctions: + - db: snapshots + input_column: id + input_value: ids + isList: true + output_column: name + output_value: name + bodyKeys: + - ids + formatEN: Delete system backup [name] + formatZH: 删除系统快照 [name] + paramKeys: [] + /settings/snapshot/description/update: + post: + consumes: + - application/json + description: 更新快照描述信息 + parameters: + - description: request + in: body + name: request + required: true + schema: + $ref: '#/definitions/dto.UpdateDescription' + responses: + "200": + description: OK + security: + - ApiKeyAuth: [] + summary: Update snapshot description + tags: + - System Setting + x-panel-log: + BeforeFunctions: + - db: snapshots + input_column: id + input_value: id + isList: false + output_column: name + output_value: name + bodyKeys: + - id + - description + formatEN: The description of the snapshot [name] is modified => [description] + formatZH: 快照 [name] 描述信息修改 [description] + paramKeys: [] + /settings/snapshot/import: + post: + consumes: + - application/json + description: 导入已有快照 + parameters: + - description: request + in: body + name: request + required: true + schema: + $ref: '#/definitions/dto.SnapshotImport' + responses: + "200": + description: OK + security: + - ApiKeyAuth: [] + summary: Import system snapshot + tags: + - System Setting + x-panel-log: + BeforeFunctions: [] + bodyKeys: + - from + - names + formatEN: Sync system snapshots [names] from [from] + formatZH: 从 [from] 同步系统快照 [names] + paramKeys: [] + /settings/snapshot/recover: + post: + consumes: + - application/json + description: 从系统快照恢复 + parameters: + - description: request + in: body + name: request + required: true + schema: + $ref: '#/definitions/dto.SnapshotRecover' + responses: + "200": + description: OK + security: + - ApiKeyAuth: [] + summary: Recover system backup + tags: + - System Setting + x-panel-log: + BeforeFunctions: + - db: snapshots + input_column: id + input_value: id + isList: false + output_column: name + output_value: name + bodyKeys: + - id + formatEN: Recover from system backup [name] + formatZH: 从系统快照 [name] 恢复 + paramKeys: [] + /settings/snapshot/rollback: + post: + consumes: + - application/json + description: 从系统快照回滚 + parameters: + - description: request + in: body + name: request + required: true + schema: + $ref: '#/definitions/dto.SnapshotRecover' + responses: + "200": + description: OK + security: + - ApiKeyAuth: [] + summary: Rollback system backup + tags: + - System Setting + x-panel-log: + BeforeFunctions: + - db: snapshots + input_column: id + input_value: id + isList: false + output_column: name + output_value: name + bodyKeys: + - id + formatEN: Rollback from system backup [name] + formatZH: 从系统快照 [name] 回滚 + paramKeys: [] + /settings/snapshot/search: + post: + consumes: + - application/json + description: 获取系统快照列表分页 + parameters: + - description: request + in: body + name: request + required: true + schema: + $ref: '#/definitions/dto.SearchWithPage' + responses: + "200": + description: OK + schema: + $ref: '#/definitions/dto.PageResult' + security: + - ApiKeyAuth: [] + summary: Page system snapshot + tags: + - System Setting + /settings/snapshot/status: + post: + consumes: + - application/json + description: 获取快照状态 + parameters: + - description: request + in: body + name: request + required: true + schema: + $ref: '#/definitions/dto.OperateByID' + responses: + "200": + description: OK + security: + - ApiKeyAuth: [] + summary: Load Snapshot status + tags: + - System Setting + /settings/ssl/download: + post: + description: 下载证书 + responses: + "200": + description: OK + security: + - ApiKeyAuth: [] + summary: Download system cert + tags: + - System Setting + /settings/ssl/info: + get: + description: 获取证书信息 + responses: + "200": + description: OK + schema: + $ref: '#/definitions/dto.SettingInfo' + security: + - ApiKeyAuth: [] + summary: Load system cert info + tags: + - System Setting + /settings/ssl/update: + post: + consumes: + - application/json + description: 修改系统 ssl 登录 + parameters: + - description: request + in: body + name: request + required: true + schema: + $ref: '#/definitions/dto.SSLUpdate' + responses: + "200": + description: OK + security: + - ApiKeyAuth: [] + summary: Update system ssl + tags: + - System Setting + x-panel-log: + BeforeFunctions: [] + bodyKeys: + - ssl + formatEN: update system ssl => [ssl] + formatZH: 修改系统 ssl => [ssl] + paramKeys: [] + /settings/update: + post: + consumes: + - application/json + description: 更新系统配置 + parameters: + - description: request + in: body + name: request + required: true + schema: + $ref: '#/definitions/dto.SettingUpdate' + responses: + "200": + description: OK + security: + - ApiKeyAuth: [] + summary: Update system setting + tags: + - System Setting + x-panel-log: + BeforeFunctions: [] + bodyKeys: + - key + - value + formatEN: update system setting [key] => [value] + formatZH: 修改系统配置 [key] => [value] + paramKeys: [] + /settings/upgrade: + get: + consumes: + - application/json + description: 获取版本 release notes + parameters: + - description: request + in: body + name: request + required: true + schema: + $ref: '#/definitions/dto.Upgrade' + responses: + "200": + description: OK + security: + - ApiKeyAuth: [] + summary: Load release notes by version + tags: + - System Setting + post: + consumes: + - application/json + description: 系统更新 + parameters: + - description: request + in: body + name: request + required: true + schema: + $ref: '#/definitions/dto.Upgrade' + responses: + "200": + description: OK + security: + - ApiKeyAuth: [] + summary: Upgrade + tags: + - System Setting + x-panel-log: + BeforeFunctions: [] + bodyKeys: + - version + formatEN: upgrade system => [version] + formatZH: 更新系统 => [version] + paramKeys: [] + /toolbox/clam: + post: + consumes: + - application/json + description: 创建扫描规则 + parameters: + - description: request + in: body + name: request + required: true + schema: + $ref: '#/definitions/dto.ClamCreate' + responses: + "200": + description: OK + security: + - ApiKeyAuth: [] + summary: Create clam + tags: + - Clam + x-panel-log: + BeforeFunctions: [] + bodyKeys: + - name + - path + formatEN: create clam [name][path] + formatZH: 创建扫描规则 [name][path] + paramKeys: [] + /toolbox/clam/base: + get: + consumes: + - application/json + description: 获取 Clam 基础信息 + responses: + "200": + description: OK + schema: + $ref: '#/definitions/dto.ClamBaseInfo' + security: + - ApiKeyAuth: [] + summary: Load clam base info + tags: + - Clam + /toolbox/clam/del: + post: + consumes: + - application/json + description: 删除扫描规则 + parameters: + - description: request + in: body + name: request + required: true + schema: + $ref: '#/definitions/dto.ClamDelete' + responses: + "200": + description: OK + security: + - ApiKeyAuth: [] + summary: Delete clam + tags: + - Clam + x-panel-log: + BeforeFunctions: + - db: clams + input_column: id + input_value: ids + isList: true + output_column: name + output_value: names + bodyKeys: + - ids + formatEN: delete clam [names] + formatZH: 删除扫描规则 [names] + paramKeys: [] + /toolbox/clam/file/search: + post: + consumes: + - application/json + description: 获取扫描文件 + parameters: + - description: request + in: body + name: request + required: true + schema: + $ref: '#/definitions/dto.ClamFileReq' + responses: + "200": + description: OK + schema: + $ref: '#/definitions/dto.PageResult' + security: + - ApiKeyAuth: [] + summary: Load clam file + tags: + - Clam + /toolbox/clam/file/update: + post: + consumes: + - application/json + description: 更新病毒扫描配置文件 + parameters: + - description: request + in: body + name: request + required: true + schema: + $ref: '#/definitions/dto.UpdateByNameAndFile' + responses: + "200": + description: OK + security: + - ApiKeyAuth: [] + summary: Update clam file + tags: + - Clam + /toolbox/clam/handle: + post: + consumes: + - application/json + description: 执行病毒扫描 + parameters: + - description: request + in: body + name: request + required: true + schema: + $ref: '#/definitions/dto.OperateByID' + responses: + "200": + description: OK + security: + - ApiKeyAuth: [] + summary: Handle clam scan + tags: + - Clam + x-panel-log: + BeforeFunctions: + - db: clams + input_column: id + input_value: id + isList: true + output_column: name + output_value: name + bodyKeys: + - id + formatEN: handle clam scan [name] + formatZH: 执行病毒扫描 [name] + paramKeys: [] + /toolbox/clam/operate: + post: + consumes: + - application/json + description: 修改 Clam 状态 + parameters: + - description: request + in: body + name: request + required: true + schema: + $ref: '#/definitions/dto.Operate' + responses: {} + security: + - ApiKeyAuth: [] + summary: Operate Clam + tags: + - Clam + x-panel-log: + BeforeFunctions: [] + bodyKeys: + - operation + formatEN: '[operation] FTP' + formatZH: '[operation] Clam' + paramKeys: [] + /toolbox/clam/record/clean: + post: + consumes: + - application/json + description: 清空扫描报告 + parameters: + - description: request + in: body + name: request + required: true + schema: + $ref: '#/definitions/dto.OperateByID' + responses: {} + security: + - ApiKeyAuth: [] + summary: Clean clam record + tags: + - Clam + x-panel-log: + BeforeFunctions: + - db: clams + input_column: id + input_value: id + isList: true + output_column: name + output_value: name + bodyKeys: + - id + formatEN: clean clam record [name] + formatZH: 清空扫描报告 [name] + paramKeys: [] + /toolbox/clam/record/log: + post: + consumes: + - application/json + description: 获取扫描结果详情 + parameters: + - description: request + in: body + name: request + required: true + schema: + $ref: '#/definitions/dto.ClamLogReq' + responses: + "200": + description: OK + security: + - ApiKeyAuth: [] + summary: Load clam record detail + tags: + - Clam + /toolbox/clam/record/search: + post: + consumes: + - application/json + description: 获取扫描结果列表分页 + parameters: + - description: request + in: body + name: request + required: true + schema: + $ref: '#/definitions/dto.ClamLogSearch' + responses: + "200": + description: OK + schema: + $ref: '#/definitions/dto.PageResult' + security: + - ApiKeyAuth: [] + summary: Page clam record + tags: + - Clam + /toolbox/clam/search: + post: + consumes: + - application/json + description: 获取扫描规则列表分页 + parameters: + - description: request + in: body + name: request + required: true + schema: + $ref: '#/definitions/dto.SearchWithPage' + responses: + "200": + description: OK + schema: + $ref: '#/definitions/dto.PageResult' + security: + - ApiKeyAuth: [] + summary: Page clam + tags: + - Clam + /toolbox/clam/status/update: + post: + consumes: + - application/json + description: 修改扫描规则状态 + parameters: + - description: request + in: body + name: request + required: true + schema: + $ref: '#/definitions/dto.ClamUpdateStatus' + responses: + "200": + description: OK + security: + - ApiKeyAuth: [] + summary: Update clam status + tags: + - Clam + x-panel-log: + BeforeFunctions: + - db: clams + input_column: id + input_value: id + isList: false + output_column: name + output_value: name + bodyKeys: + - id + - status + formatEN: change the status of clam [name] to [status]. + formatZH: 修改扫描规则 [name] 状态为 [status] + paramKeys: [] + /toolbox/clam/update: + post: + consumes: + - application/json + description: 修改扫描规则 + parameters: + - description: request + in: body + name: request + required: true + schema: + $ref: '#/definitions/dto.ClamUpdate' + responses: + "200": + description: OK + security: + - ApiKeyAuth: [] + summary: Update clam + tags: + - Clam + x-panel-log: + BeforeFunctions: [] + bodyKeys: + - name + - path + formatEN: update clam [name][path] + formatZH: 修改扫描规则 [name][path] + paramKeys: [] + /toolbox/clean: + post: + consumes: + - application/json + description: 清理系统垃圾文件 + parameters: + - description: request + in: body + name: request + required: true + schema: + items: + $ref: '#/definitions/dto.Clean' + type: array + responses: + "200": + description: OK + security: + - ApiKeyAuth: [] + summary: Clean system + tags: + - Device + x-panel-log: + BeforeFunctions: [] + bodyKeys: [] + formatEN: Clean system junk files + formatZH: 清理系统垃圾文件 + paramKeys: [] + /toolbox/device/base: + post: + description: 获取设备基础信息 + responses: + "200": + description: OK + schema: + $ref: '#/definitions/dto.DeviceBaseInfo' + security: + - ApiKeyAuth: [] + summary: Load device base info + tags: + - Device + /toolbox/device/check/dns: + post: + consumes: + - application/json + description: 检查系统 DNS 配置可用性 + parameters: + - description: request + in: body + name: request + required: true + schema: + $ref: '#/definitions/dto.SettingUpdate' + responses: + "200": + description: OK + security: + - ApiKeyAuth: [] + summary: Check device DNS conf + tags: + - Device + /toolbox/device/conf: + post: + consumes: + - application/json + description: 获取系统配置文件 + parameters: + - description: request + in: body + name: request + required: true + schema: + $ref: '#/definitions/dto.OperationWithName' + responses: + "200": + description: OK + security: + - ApiKeyAuth: [] + summary: load conf + tags: + - Device + /toolbox/device/update/byconf: + post: + consumes: + - application/json + description: 通过文件修改配置 + parameters: + - description: request + in: body + name: request + required: true + schema: + $ref: '#/definitions/dto.UpdateByNameAndFile' + responses: + "200": + description: OK + security: + - ApiKeyAuth: [] + summary: Update device conf by file + tags: + - Device + /toolbox/device/update/conf: + post: + consumes: + - application/json + description: 修改系统参数 + parameters: + - description: request + in: body + name: request + required: true + schema: + $ref: '#/definitions/dto.SettingUpdate' + responses: + "200": + description: OK + security: + - ApiKeyAuth: [] + summary: Update device + tags: + - Device + x-panel-log: + BeforeFunctions: [] + bodyKeys: + - key + - value + formatEN: update device conf [key] => [value] + formatZH: 修改主机参数 [key] => [value] + paramKeys: [] + /toolbox/device/update/host: + post: + description: 修改系统 hosts + responses: + "200": + description: OK + security: + - ApiKeyAuth: [] + summary: Update device hosts + tags: + - Device + x-panel-log: + BeforeFunctions: [] + bodyKeys: + - key + - value + formatEN: update device host [key] => [value] + formatZH: 修改主机 Host [key] => [value] + paramKeys: [] + /toolbox/device/update/passwd: + post: + consumes: + - application/json + description: 修改系统密码 + parameters: + - description: request + in: body + name: request + required: true + schema: + $ref: '#/definitions/dto.ChangePasswd' + responses: + "200": + description: OK + security: + - ApiKeyAuth: [] + summary: Update device passwd + tags: + - Device + /toolbox/device/update/swap: + post: + consumes: + - application/json + description: 修改系统 Swap + parameters: + - description: request + in: body + name: request + required: true + schema: + $ref: '#/definitions/dto.SwapHelper' + responses: + "200": + description: OK + security: + - ApiKeyAuth: [] + summary: Update device swap + tags: + - Device + x-panel-log: + BeforeFunctions: [] + bodyKeys: + - operate + - path + formatEN: '[operate] device swap [path]' + formatZH: '[operate] 主机 swap [path]' + paramKeys: [] + /toolbox/device/zone/options: + get: + consumes: + - application/json + description: 获取系统可用时区选项 + responses: + "200": + description: OK + schema: + type: Array + security: + - ApiKeyAuth: [] + summary: list time zone options + tags: + - Device + /toolbox/fail2ban/base: + get: + description: 获取 Fail2ban 基础信息 + responses: + "200": + description: OK + schema: + $ref: '#/definitions/dto.Fail2BanBaseInfo' + security: + - ApiKeyAuth: [] + summary: Load fail2ban base info + tags: + - Fail2ban + /toolbox/fail2ban/load/conf: + get: + consumes: + - application/json + description: 获取 fail2ban 配置文件 + responses: + "200": + description: OK + security: + - ApiKeyAuth: [] + summary: Load fail2ban conf + tags: + - Fail2ban + /toolbox/fail2ban/operate: + post: + consumes: + - application/json + description: 修改 Fail2ban 状态 + parameters: + - description: request + in: body + name: request + required: true + schema: + $ref: '#/definitions/dto.Operate' + responses: {} + security: + - ApiKeyAuth: [] + summary: Operate fail2ban + tags: + - Fail2ban + x-panel-log: + BeforeFunctions: [] + bodyKeys: + - operation + formatEN: '[operation] Fail2ban' + formatZH: '[operation] Fail2ban' + paramKeys: [] + /toolbox/fail2ban/operate/sshd: + post: + consumes: + - application/json + description: 配置 sshd + parameters: + - description: request + in: body + name: request + required: true + schema: + $ref: '#/definitions/dto.Operate' + responses: {} + security: + - ApiKeyAuth: [] + summary: Operate sshd of fail2ban + tags: + - Fail2ban + /toolbox/fail2ban/search: + post: + consumes: + - application/json + description: 获取 Fail2ban ip + parameters: + - description: request + in: body + name: request + required: true + schema: + $ref: '#/definitions/dto.Fail2BanSearch' + responses: + "200": + description: OK + schema: + type: Array + security: + - ApiKeyAuth: [] + summary: Page fail2ban ip list + tags: + - Fail2ban + /toolbox/fail2ban/update: + post: + consumes: + - application/json + description: 修改 Fail2ban 配置 + parameters: + - description: request + in: body + name: request + required: true + schema: + $ref: '#/definitions/dto.Fail2BanUpdate' + responses: + "200": + description: OK + security: + - ApiKeyAuth: [] + summary: Update fail2ban conf + tags: + - Fail2ban + x-panel-log: + BeforeFunctions: [] + bodyKeys: + - key + - value + formatEN: update fail2ban conf [key] => [value] + formatZH: 修改 Fail2ban 配置 [key] => [value] + paramKeys: [] + /toolbox/fail2ban/update/byconf: + post: + consumes: + - application/json + description: 通过文件修改 fail2ban 配置 + parameters: + - description: request + in: body + name: request + required: true + schema: + $ref: '#/definitions/dto.UpdateByFile' + responses: + "200": + description: OK + security: + - ApiKeyAuth: [] + summary: Update fail2ban conf by file + tags: + - Fail2ban + /toolbox/ftp: + post: + consumes: + - application/json + description: 创建 FTP 账户 + parameters: + - description: request + in: body + name: request + required: true + schema: + $ref: '#/definitions/dto.FtpCreate' + responses: + "200": + description: OK + security: + - ApiKeyAuth: [] + summary: Create FTP user + tags: + - FTP + x-panel-log: + BeforeFunctions: [] + bodyKeys: + - user + - path + formatEN: create FTP [user][path] + formatZH: 创建 FTP 账户 [user][path] + paramKeys: [] + /toolbox/ftp/base: + get: + description: 获取 FTP 基础信息 + responses: + "200": + description: OK + schema: + $ref: '#/definitions/dto.FtpBaseInfo' + security: + - ApiKeyAuth: [] + summary: Load FTP base info + tags: + - FTP + /toolbox/ftp/del: + post: + consumes: + - application/json + description: 删除 FTP 账户 + parameters: + - description: request + in: body + name: request + required: true + schema: + $ref: '#/definitions/dto.BatchDeleteReq' + responses: + "200": + description: OK + security: + - ApiKeyAuth: [] + summary: Delete FTP user + tags: + - FTP + x-panel-log: + BeforeFunctions: + - db: ftps + input_column: id + input_value: ids + isList: true + output_column: user + output_value: users + bodyKeys: + - ids + formatEN: delete FTP users [users] + formatZH: 删除 FTP 账户 [users] + paramKeys: [] + /toolbox/ftp/log/search: + post: + consumes: + - application/json + description: 获取 FTP 操作日志 + parameters: + - description: request + in: body + name: request + required: true + schema: + $ref: '#/definitions/dto.FtpLogSearch' + responses: + "200": + description: OK + schema: + $ref: '#/definitions/dto.PageResult' + security: + - ApiKeyAuth: [] + summary: Load FTP operation log + tags: + - FTP + /toolbox/ftp/operate: + post: + consumes: + - application/json + description: 修改 FTP 状态 + parameters: + - description: request + in: body + name: request + required: true + schema: + $ref: '#/definitions/dto.Operate' + responses: {} + security: + - ApiKeyAuth: [] + summary: Operate FTP + tags: + - FTP + x-panel-log: + BeforeFunctions: [] + bodyKeys: + - operation + formatEN: '[operation] FTP' + formatZH: '[operation] FTP' + paramKeys: [] + /toolbox/ftp/search: + post: + consumes: + - application/json + description: 获取 FTP 账户列表分页 + parameters: + - description: request + in: body + name: request + required: true + schema: + $ref: '#/definitions/dto.SearchWithPage' + responses: + "200": + description: OK + schema: + $ref: '#/definitions/dto.PageResult' + security: + - ApiKeyAuth: [] + summary: Page FTP user + tags: + - FTP + /toolbox/ftp/sync: + post: + consumes: + - application/json + description: 同步 FTP 账户 + parameters: + - description: request + in: body + name: request + required: true + schema: + $ref: '#/definitions/dto.BatchDeleteReq' + responses: + "200": + description: OK + security: + - ApiKeyAuth: [] + summary: Sync FTP user + tags: + - FTP + x-panel-log: + BeforeFunctions: [] + bodyKeys: [] + formatEN: sync FTP users + formatZH: 同步 FTP 账户 + paramKeys: [] + /toolbox/ftp/update: + post: + consumes: + - application/json + description: 修改 FTP 账户 + parameters: + - description: request + in: body + name: request + required: true + schema: + $ref: '#/definitions/dto.FtpUpdate' + responses: + "200": + description: OK + security: + - ApiKeyAuth: [] + summary: Update FTP user + tags: + - FTP + x-panel-log: + BeforeFunctions: [] + bodyKeys: + - user + - path + formatEN: update FTP [user][path] + formatZH: 修改 FTP 账户 [user][path] + paramKeys: [] + /toolbox/scan: + post: + description: 扫描系统垃圾文件 + responses: + "200": + description: OK + security: + - ApiKeyAuth: [] + summary: Scan system + tags: + - Device + x-panel-log: + BeforeFunctions: [] + bodyKeys: [] + formatEN: scan System Junk Files + formatZH: 扫描系统垃圾文件 + paramKeys: [] + /websites: + post: + consumes: + - application/json + description: 创建网站 + parameters: + - description: request + in: body + name: request + required: true + schema: + $ref: '#/definitions/request.WebsiteCreate' + responses: + "200": + description: OK + security: + - ApiKeyAuth: [] + summary: Create website + tags: + - Website + x-panel-log: + BeforeFunctions: [] + bodyKeys: + - primaryDomain + formatEN: Create website [primaryDomain] + formatZH: 创建网站 [primaryDomain] + paramKeys: [] + /websites/:id: + get: + consumes: + - application/json + description: 通过 id 查询网站 + parameters: + - description: request + in: path + name: id + required: true + type: integer + responses: + "200": + description: OK + schema: + $ref: '#/definitions/response.WebsiteDTO' + security: + - ApiKeyAuth: [] + summary: Search website by id + tags: + - Website + /websites/:id/config/:type: + get: + consumes: + - application/json + description: 通过 id 查询网站 nginx + parameters: + - description: request + in: path + name: id + required: true + type: integer + responses: + "200": + description: OK + schema: + $ref: '#/definitions/response.FileInfo' + security: + - ApiKeyAuth: [] + summary: Search website nginx by id + tags: + - Website Nginx + /websites/:id/https: + get: + consumes: + - application/json + description: 获取 https 配置 + parameters: + - description: request + in: path + name: id + required: true + type: integer + responses: + "200": + description: OK + schema: + $ref: '#/definitions/response.WebsiteHTTPS' + security: + - ApiKeyAuth: [] + summary: Load https conf + tags: + - Website HTTPS + post: + consumes: + - application/json + description: 更新 https 配置 + parameters: + - description: request + in: body + name: request + required: true + schema: + $ref: '#/definitions/request.WebsiteHTTPSOp' + responses: + "200": + description: OK + schema: + $ref: '#/definitions/response.WebsiteHTTPS' + security: + - ApiKeyAuth: [] + summary: Update https conf + tags: + - Website HTTPS + x-panel-log: + BeforeFunctions: + - db: websites + input_column: id + input_value: websiteId + isList: false + output_column: primary_domain + output_value: domain + bodyKeys: + - websiteId + formatEN: Update website https [domain] conf + formatZH: 更新网站 [domain] https 配置 + paramKeys: [] + /websites/acme: + post: + consumes: + - application/json + description: 创建网站 acme + parameters: + - description: request + in: body + name: request + required: true + schema: + $ref: '#/definitions/request.WebsiteAcmeAccountCreate' + responses: + "200": + description: OK + schema: + $ref: '#/definitions/response.WebsiteAcmeAccountDTO' + security: + - ApiKeyAuth: [] + summary: Create website acme account + tags: + - Website Acme + x-panel-log: + BeforeFunctions: [] + bodyKeys: + - email + formatEN: Create website acme [email] + formatZH: 创建网站 acme [email] + paramKeys: [] + /websites/acme/del: + post: + consumes: + - application/json + description: 删除网站 acme + parameters: + - description: request + in: body + name: request + required: true + schema: + $ref: '#/definitions/request.WebsiteResourceReq' + responses: + "200": + description: OK + security: + - ApiKeyAuth: [] + summary: Delete website acme account + tags: + - Website Acme + x-panel-log: + BeforeFunctions: + - db: website_acme_accounts + input_column: id + input_value: id + isList: false + output_column: email + output_value: email + bodyKeys: + - id + formatEN: Delete website acme [email] + formatZH: 删除网站 acme [email] + paramKeys: [] + /websites/acme/search: + post: + consumes: + - application/json + description: 获取网站 acme 列表分页 + parameters: + - description: request + in: body + name: request + required: true + schema: + $ref: '#/definitions/dto.PageInfo' + responses: + "200": + description: OK + schema: + $ref: '#/definitions/dto.PageResult' + security: + - ApiKeyAuth: [] + summary: Page website acme accounts + tags: + - Website Acme + /websites/auths: + post: + consumes: + - application/json + description: 获取密码访问配置 + parameters: + - description: request + in: body + name: request + required: true + schema: + $ref: '#/definitions/request.NginxAuthReq' + responses: + "200": + description: OK + security: + - ApiKeyAuth: [] + summary: Get AuthBasic conf + tags: + - Website + /websites/auths/update: + post: + consumes: + - application/json + description: 更新密码访问配置 + parameters: + - description: request + in: body + name: request + required: true + schema: + $ref: '#/definitions/request.NginxAuthUpdate' + responses: + "200": + description: OK + security: + - ApiKeyAuth: [] + summary: Get AuthBasic conf + tags: + - Website + /websites/ca: + post: + consumes: + - application/json + description: 创建网站 ca + parameters: + - description: request + in: body + name: request + required: true + schema: + $ref: '#/definitions/request.WebsiteCACreate' + responses: + "200": + description: OK + schema: + $ref: '#/definitions/request.WebsiteCACreate' + security: + - ApiKeyAuth: [] + summary: Create website ca + tags: + - Website CA + x-panel-log: + BeforeFunctions: [] + bodyKeys: + - name + formatEN: Create website ca [name] + formatZH: 创建网站 ca [name] + paramKeys: [] + /websites/ca/{id}: + get: + consumes: + - application/json + description: 获取网站 ca + parameters: + - description: id + in: path + name: id + required: true + type: integer + responses: + "200": + description: OK + schema: + $ref: '#/definitions/response.WebsiteCADTO' + security: + - ApiKeyAuth: [] + summary: Get website ca + tags: + - Website CA + /websites/ca/del: + post: + consumes: + - application/json + description: 删除网站 ca + parameters: + - description: request + in: body + name: request + required: true + schema: + $ref: '#/definitions/request.WebsiteCommonReq' + responses: + "200": + description: OK + security: + - ApiKeyAuth: [] + summary: Delete website ca + tags: + - Website CA + x-panel-log: + BeforeFunctions: + - db: website_cas + input_column: id + input_value: id + isList: false + output_column: name + output_value: name + bodyKeys: + - id + formatEN: Delete website ca [name] + formatZH: 删除网站 ca [name] + paramKeys: [] + /websites/ca/download: + post: + consumes: + - application/json + description: 下载 CA 证书文件 + parameters: + - description: request + in: body + name: request + required: true + schema: + $ref: '#/definitions/request.WebsiteResourceReq' + responses: + "200": + description: OK + security: + - ApiKeyAuth: [] + summary: Download CA file + tags: + - Website CA + x-panel-log: + BeforeFunctions: + - db: website_cas + input_column: id + input_value: id + isList: false + output_column: name + output_value: name + bodyKeys: + - id + formatEN: download ca file [name] + formatZH: 下载 CA 证书文件 [name] + paramKeys: [] + /websites/ca/obtain: + post: + consumes: + - application/json + description: 自签 SSL 证书 + parameters: + - description: request + in: body + name: request + required: true + schema: + $ref: '#/definitions/request.WebsiteCAObtain' + responses: + "200": + description: OK + security: + - ApiKeyAuth: [] + summary: Obtain SSL + tags: + - Website CA + x-panel-log: + BeforeFunctions: + - db: website_cas + input_column: id + input_value: id + isList: false + output_column: name + output_value: name + bodyKeys: + - id + formatEN: Obtain SSL [name] + formatZH: 自签 SSL 证书 [name] + paramKeys: [] + /websites/ca/renew: + post: + consumes: + - application/json + description: 续签 SSL 证书 + parameters: + - description: request + in: body + name: request + required: true + schema: + $ref: '#/definitions/request.WebsiteCAObtain' + responses: + "200": + description: OK + security: + - ApiKeyAuth: [] + summary: Obtain SSL + tags: + - Website CA + x-panel-log: + BeforeFunctions: + - db: website_cas + input_column: id + input_value: id + isList: false + output_column: name + output_value: name + bodyKeys: + - id + formatEN: Obtain SSL [name] + formatZH: 自签 SSL 证书 [name] + paramKeys: [] + /websites/ca/search: + post: + consumes: + - application/json + description: 获取网站 ca 列表分页 + parameters: + - description: request + in: body + name: request + required: true + schema: + $ref: '#/definitions/request.WebsiteCASearch' + responses: + "200": + description: OK + schema: + $ref: '#/definitions/dto.PageResult' + security: + - ApiKeyAuth: [] + summary: Page website ca + tags: + - Website CA + /websites/check: + post: + consumes: + - application/json + description: 网站创建前检查 + parameters: + - description: request + in: body + name: request + required: true + schema: + $ref: '#/definitions/request.WebsiteInstallCheckReq' + responses: + "200": + description: OK + schema: + items: + $ref: '#/definitions/response.WebsitePreInstallCheck' + type: array + security: + - ApiKeyAuth: [] + summary: Check before create website + tags: + - Website + /websites/config: + post: + consumes: + - application/json + description: 获取 nginx 配置 + parameters: + - description: request + in: body + name: request + required: true + schema: + $ref: '#/definitions/request.NginxScopeReq' + responses: + "200": + description: OK + schema: + $ref: '#/definitions/response.WebsiteNginxConfig' + security: + - ApiKeyAuth: [] + summary: Load nginx conf + tags: + - Website Nginx + /websites/config/update: + post: + consumes: + - application/json + description: 更新 nginx 配置 + parameters: + - description: request + in: body + name: request + required: true + schema: + $ref: '#/definitions/request.NginxConfigUpdate' + responses: + "200": + description: OK + security: + - ApiKeyAuth: [] + summary: Update nginx conf + tags: + - Website Nginx + x-panel-log: + BeforeFunctions: + - db: websites + input_column: id + input_value: websiteId + isList: false + output_column: primary_domain + output_value: domain + bodyKeys: + - websiteId + formatEN: Nginx conf update [domain] + formatZH: nginx 配置修改 [domain] + paramKeys: [] + /websites/default/html/:type: + get: + consumes: + - application/json + description: 获取默认 html + responses: + "200": + description: OK + schema: + $ref: '#/definitions/response.FileInfo' + security: + - ApiKeyAuth: [] + summary: Get default html + tags: + - Website + /websites/default/html/update: + post: + consumes: + - application/json + description: 更新默认 html + parameters: + - description: request + in: body + name: request + required: true + schema: + $ref: '#/definitions/request.WebsiteHtmlUpdate' + responses: + "200": + description: OK + security: + - ApiKeyAuth: [] + summary: Update default html + tags: + - Website + x-panel-log: + BeforeFunctions: [] + bodyKeys: + - type + formatEN: Update default html + formatZH: 更新默认 html + paramKeys: [] + /websites/default/server: + post: + consumes: + - application/json + description: 操作网站日志 + parameters: + - description: request + in: body + name: request + required: true + schema: + $ref: '#/definitions/request.WebsiteDefaultUpdate' + responses: + "200": + description: OK + security: + - ApiKeyAuth: [] + summary: Change default server + tags: + - Website + x-panel-log: + BeforeFunctions: + - db: websites + input_column: id + input_value: id + isList: false + output_column: primary_domain + output_value: domain + bodyKeys: + - id + - operate + formatEN: Change default server => [domain] + formatZH: 修改默认 server => [domain] + paramKeys: [] + /websites/del: + post: + consumes: + - application/json + description: 删除网站 + parameters: + - description: request + in: body + name: request + required: true + schema: + $ref: '#/definitions/request.WebsiteDelete' + responses: + "200": + description: OK + security: + - ApiKeyAuth: [] + summary: Delete website + tags: + - Website + x-panel-log: + BeforeFunctions: + - db: websites + input_column: id + input_value: id + isList: false + output_column: primary_domain + output_value: domain + bodyKeys: + - id + formatEN: Delete website [domain] + formatZH: 删除网站 [domain] + paramKeys: [] + /websites/dir: + post: + consumes: + - application/json + description: 获取网站目录配置 + parameters: + - description: request + in: body + name: request + required: true + schema: + $ref: '#/definitions/request.WebsiteCommonReq' + responses: + "200": + description: OK + security: + - ApiKeyAuth: [] + summary: Get website dir + tags: + - Website + /websites/dir/permission: + post: + consumes: + - application/json + description: 更新网站目录权限 + parameters: + - description: request + in: body + name: request + required: true + schema: + $ref: '#/definitions/request.WebsiteUpdateDirPermission' + responses: + "200": + description: OK + security: + - ApiKeyAuth: [] + summary: Update Site Dir permission + tags: + - Website + x-panel-log: + BeforeFunctions: + - db: websites + input_column: id + input_value: id + isList: false + output_column: primary_domain + output_value: domain + bodyKeys: + - id + formatEN: Update domain [domain] dir permission + formatZH: 更新网站 [domain] 目录权限 + paramKeys: [] + /websites/dir/update: + post: + consumes: + - application/json + description: 更新网站目录 + parameters: + - description: request + in: body + name: request + required: true + schema: + $ref: '#/definitions/request.WebsiteUpdateDir' + responses: + "200": + description: OK + security: + - ApiKeyAuth: [] + summary: Update Site Dir + tags: + - Website + x-panel-log: + BeforeFunctions: + - db: websites + input_column: id + input_value: id + isList: false + output_column: primary_domain + output_value: domain + bodyKeys: + - id + formatEN: Update domain [domain] dir + formatZH: 更新网站 [domain] 目录 + paramKeys: [] + /websites/dns: + post: + consumes: + - application/json + description: 创建网站 dns + parameters: + - description: request + in: body + name: request + required: true + schema: + $ref: '#/definitions/request.WebsiteDnsAccountCreate' + responses: + "200": + description: OK + security: + - ApiKeyAuth: [] + summary: Create website dns account + tags: + - Website DNS + x-panel-log: + BeforeFunctions: [] + bodyKeys: + - name + formatEN: Create website dns [name] + formatZH: 创建网站 dns [name] + paramKeys: [] + /websites/dns/del: + post: + consumes: + - application/json + description: 删除网站 dns + parameters: + - description: request + in: body + name: request + required: true + schema: + $ref: '#/definitions/request.WebsiteResourceReq' + responses: + "200": + description: OK + security: + - ApiKeyAuth: [] + summary: Delete website dns account + tags: + - Website DNS + x-panel-log: + BeforeFunctions: + - db: website_dns_accounts + input_column: id + input_value: id + isList: false + output_column: name + output_value: name + bodyKeys: + - id + formatEN: Delete website dns [name] + formatZH: 删除网站 dns [name] + paramKeys: [] + /websites/dns/search: + post: + consumes: + - application/json + description: 获取网站 dns 列表分页 + parameters: + - description: request + in: body + name: request + required: true + schema: + $ref: '#/definitions/dto.PageInfo' + responses: + "200": + description: OK + schema: + $ref: '#/definitions/dto.PageResult' + security: + - ApiKeyAuth: [] + summary: Page website dns accounts + tags: + - Website DNS + /websites/dns/update: + post: + consumes: + - application/json + description: 更新网站 dns + parameters: + - description: request + in: body + name: request + required: true + schema: + $ref: '#/definitions/request.WebsiteDnsAccountUpdate' + responses: + "200": + description: OK + security: + - ApiKeyAuth: [] + summary: Update website dns account + tags: + - Website DNS + x-panel-log: + BeforeFunctions: [] + bodyKeys: + - name + formatEN: Update website dns [name] + formatZH: 更新网站 dns [name] + paramKeys: [] + /websites/domains: + post: + consumes: + - application/json + description: 创建网站域名 + parameters: + - description: request + in: body + name: request + required: true + schema: + $ref: '#/definitions/request.WebsiteDomainCreate' + responses: + "200": + description: OK + schema: + $ref: '#/definitions/model.WebsiteDomain' + security: + - ApiKeyAuth: [] + summary: Create website domain + tags: + - Website Domain + x-panel-log: + BeforeFunctions: [] + bodyKeys: + - domain + formatEN: Create domain [domain] + formatZH: 创建域名 [domain] + paramKeys: [] + /websites/domains/:websiteId: + get: + consumes: + - application/json + description: 通过网站 id 查询域名 + parameters: + - description: request + in: path + name: websiteId + required: true + type: integer + responses: + "200": + description: OK + schema: + items: + $ref: '#/definitions/model.WebsiteDomain' + type: array + security: + - ApiKeyAuth: [] + summary: Search website domains by websiteId + tags: + - Website Domain + /websites/domains/del: + post: + consumes: + - application/json + description: 删除网站域名 + parameters: + - description: request + in: body + name: request + required: true + schema: + $ref: '#/definitions/request.WebsiteDomainDelete' + responses: + "200": + description: OK + security: + - ApiKeyAuth: [] + summary: Delete website domain + tags: + - Website Domain + x-panel-log: + BeforeFunctions: + - db: website_domains + input_column: id + input_value: id + isList: false + output_column: domain + output_value: domain + bodyKeys: + - id + formatEN: Delete domain [domain] + formatZH: 删除域名 [domain] + paramKeys: [] + /websites/leech: + post: + consumes: + - application/json + description: 获取防盗链配置 + parameters: + - description: request + in: body + name: request + required: true + schema: + $ref: '#/definitions/request.NginxCommonReq' + responses: + "200": + description: OK + security: + - ApiKeyAuth: [] + summary: Get AntiLeech conf + tags: + - Website + /websites/leech/update: + post: + consumes: + - application/json + description: 更新防盗链配置 + parameters: + - description: request + in: body + name: request + required: true + schema: + $ref: '#/definitions/request.NginxAntiLeechUpdate' + responses: + "200": + description: OK + security: + - ApiKeyAuth: [] + summary: Update AntiLeech + tags: + - Website + /websites/list: + get: + description: 获取网站列表 + responses: + "200": + description: OK + schema: + items: + $ref: '#/definitions/response.WebsiteDTO' + type: array + security: + - ApiKeyAuth: [] + summary: List websites + tags: + - Website + /websites/log: + post: + consumes: + - application/json + description: 操作网站日志 + parameters: + - description: request + in: body + name: request + required: true + schema: + $ref: '#/definitions/request.WebsiteLogReq' + responses: + "200": + description: OK + schema: + $ref: '#/definitions/response.WebsiteLog' + security: + - ApiKeyAuth: [] + summary: Operate website log + tags: + - Website + x-panel-log: + BeforeFunctions: + - db: websites + input_column: id + input_value: id + isList: false + output_column: primary_domain + output_value: domain + bodyKeys: + - id + - operate + formatEN: '[domain][operate] logs' + formatZH: '[domain][operate] 日志' + paramKeys: [] + /websites/nginx/update: + post: + consumes: + - application/json + description: 更新 网站 nginx 配置 + parameters: + - description: request + in: body + name: request + required: true + schema: + $ref: '#/definitions/request.WebsiteNginxUpdate' + responses: + "200": + description: OK + security: + - ApiKeyAuth: [] + summary: Update website nginx conf + tags: + - Website Nginx + x-panel-log: + BeforeFunctions: + - db: websites + input_column: id + input_value: id + isList: false + output_column: primary_domain + output_value: domain + bodyKeys: + - id + formatEN: '[domain] Nginx conf update' + formatZH: '[domain] Nginx 配置修改' + paramKeys: [] + /websites/operate: + post: + consumes: + - application/json + description: 操作网站 + parameters: + - description: request + in: body + name: request + required: true + schema: + $ref: '#/definitions/request.WebsiteOp' + responses: + "200": + description: OK + security: + - ApiKeyAuth: [] + summary: Operate website + tags: + - Website + x-panel-log: + BeforeFunctions: + - db: websites + input_column: id + input_value: id + isList: false + output_column: primary_domain + output_value: domain + bodyKeys: + - id + - operate + formatEN: '[operate] website [domain]' + formatZH: '[operate] 网站 [domain]' + paramKeys: [] + /websites/options: + get: + description: 获取网站列表 + responses: + "200": + description: OK + schema: + items: + type: string + type: array + security: + - ApiKeyAuth: [] + summary: List website names + tags: + - Website + /websites/php/config: + post: + consumes: + - application/json + description: 更新 网站 PHP 配置 + parameters: + - description: request + in: body + name: request + required: true + schema: + $ref: '#/definitions/request.WebsitePHPConfigUpdate' + responses: + "200": + description: OK + security: + - ApiKeyAuth: [] + summary: Update website php conf + tags: + - Website PHP + x-panel-log: + BeforeFunctions: + - db: websites + input_column: id + input_value: id + isList: false + output_column: primary_domain + output_value: domain + bodyKeys: + - id + formatEN: '[domain] PHP conf update' + formatZH: '[domain] PHP 配置修改' + paramKeys: [] + /websites/php/config/:id: + get: + consumes: + - application/json + description: 获取网站 php 配置 + parameters: + - description: request + in: path + name: id + required: true + type: integer + responses: + "200": + description: OK + schema: + $ref: '#/definitions/response.PHPConfig' + security: + - ApiKeyAuth: [] + summary: Load website php conf + tags: + - Website + /websites/php/update: + post: + consumes: + - application/json + description: 更新 php 配置文件 + parameters: + - description: request + in: body + name: request + required: true + schema: + $ref: '#/definitions/request.WebsitePHPFileUpdate' + responses: + "200": + description: OK + security: + - ApiKeyAuth: [] + summary: Update php conf + tags: + - Website PHP + x-panel-log: + BeforeFunctions: + - db: websites + input_column: id + input_value: websiteId + isList: false + output_column: primary_domain + output_value: domain + bodyKeys: + - websiteId + formatEN: Nginx conf update [domain] + formatZH: php 配置修改 [domain] + paramKeys: [] + /websites/php/version: + post: + consumes: + - application/json + description: 变更 php 版本 + parameters: + - description: request + in: body + name: request + required: true + schema: + $ref: '#/definitions/request.WebsitePHPVersionReq' + responses: + "200": + description: OK + security: + - ApiKeyAuth: [] + summary: Update php version + tags: + - Website PHP + x-panel-log: + BeforeFunctions: + - db: websites + input_column: id + input_value: websiteId + isList: false + output_column: primary_domain + output_value: domain + bodyKeys: + - websiteId + formatEN: php version update [domain] + formatZH: php 版本变更 [domain] + paramKeys: [] + /websites/proxies: + post: + consumes: + - application/json + description: 获取反向代理配置 + parameters: + - description: request + in: body + name: request + required: true + schema: + $ref: '#/definitions/request.WebsiteProxyReq' + responses: + "200": + description: OK + security: + - ApiKeyAuth: [] + summary: Get proxy conf + tags: + - Website + /websites/proxies/update: + post: + consumes: + - application/json + description: 修改反向代理配置 + parameters: + - description: request + in: body + name: request + required: true + schema: + $ref: '#/definitions/request.WebsiteProxyConfig' + responses: + "200": + description: OK + security: + - ApiKeyAuth: [] + summary: Update proxy conf + tags: + - Website + x-panel-log: + BeforeFunctions: + - db: websites + input_column: id + input_value: id + isList: false + output_column: primary_domain + output_value: domain + bodyKeys: + - id + formatEN: Update domain [domain] proxy config + formatZH: '修改网站 [domain] 反向代理配置 ' + paramKeys: [] + /websites/proxy/file: + post: + consumes: + - application/json + description: 更新反向代理文件 + parameters: + - description: request + in: body + name: request + required: true + schema: + $ref: '#/definitions/request.NginxProxyUpdate' + responses: + "200": + description: OK + security: + - ApiKeyAuth: [] + summary: Update proxy file + tags: + - Website + x-panel-log: + BeforeFunctions: + - db: websites + input_column: id + input_value: websiteID + isList: false + output_column: primary_domain + output_value: domain + bodyKeys: + - websiteID + formatEN: Nginx conf proxy file update [domain] + formatZH: 更新反向代理文件 [domain] + paramKeys: [] + /websites/redirect: + post: + consumes: + - application/json + description: 获取重定向配置 + parameters: + - description: request + in: body + name: request + required: true + schema: + $ref: '#/definitions/request.WebsiteProxyReq' + responses: + "200": + description: OK + security: + - ApiKeyAuth: [] + summary: Get redirect conf + tags: + - Website + /websites/redirect/file: + post: + consumes: + - application/json + description: 更新重定向文件 + parameters: + - description: request + in: body + name: request + required: true + schema: + $ref: '#/definitions/request.NginxRedirectUpdate' + responses: + "200": + description: OK + security: + - ApiKeyAuth: [] + summary: Update redirect file + tags: + - Website + x-panel-log: + BeforeFunctions: + - db: websites + input_column: id + input_value: websiteID + isList: false + output_column: primary_domain + output_value: domain + bodyKeys: + - websiteID + formatEN: Nginx conf redirect file update [domain] + formatZH: 更新重定向文件 [domain] + paramKeys: [] + /websites/redirect/update: + post: + consumes: + - application/json + description: 修改重定向配置 + parameters: + - description: request + in: body + name: request + required: true + schema: + $ref: '#/definitions/request.NginxRedirectReq' + responses: + "200": + description: OK + security: + - ApiKeyAuth: [] + summary: Update redirect conf + tags: + - Website + x-panel-log: + BeforeFunctions: + - db: websites + input_column: id + input_value: websiteID + isList: false + output_column: primary_domain + output_value: domain + bodyKeys: + - websiteID + formatEN: Update domain [domain] redirect config + formatZH: '修改网站 [domain] 重定向理配置 ' + paramKeys: [] + /websites/rewrite: + post: + consumes: + - application/json + description: 获取伪静态配置 + parameters: + - description: request + in: body + name: request + required: true + schema: + $ref: '#/definitions/request.NginxRewriteReq' + responses: + "200": + description: OK + security: + - ApiKeyAuth: [] + summary: Get rewrite conf + tags: + - Website + /websites/rewrite/update: + post: + consumes: + - application/json + description: 更新伪静态配置 + parameters: + - description: request + in: body + name: request + required: true + schema: + $ref: '#/definitions/request.NginxRewriteUpdate' + responses: + "200": + description: OK + security: + - ApiKeyAuth: [] + summary: Update rewrite conf + tags: + - Website + x-panel-log: + BeforeFunctions: + - db: websites + input_column: id + input_value: websiteID + isList: false + output_column: primary_domain + output_value: domain + bodyKeys: + - websiteID + formatEN: Nginx conf rewrite update [domain] + formatZH: 伪静态配置修改 [domain] + paramKeys: [] + /websites/search: + post: + consumes: + - application/json + description: 获取网站列表分页 + parameters: + - description: request + in: body + name: request + required: true + schema: + $ref: '#/definitions/request.WebsiteSearch' + responses: + "200": + description: OK + schema: + $ref: '#/definitions/dto.PageResult' + security: + - ApiKeyAuth: [] + summary: Page websites + tags: + - Website + /websites/ssl: + post: + consumes: + - application/json + description: 创建网站 ssl + parameters: + - description: request + in: body + name: request + required: true + schema: + $ref: '#/definitions/request.WebsiteSSLCreate' + responses: + "200": + description: OK + schema: + $ref: '#/definitions/request.WebsiteSSLCreate' + security: + - ApiKeyAuth: [] + summary: Create website ssl + tags: + - Website SSL + x-panel-log: + BeforeFunctions: [] + bodyKeys: + - primaryDomain + formatEN: Create website ssl [primaryDomain] + formatZH: 创建网站 ssl [primaryDomain] + paramKeys: [] + /websites/ssl/:id: + get: + consumes: + - application/json + description: 通过 id 查询 ssl + parameters: + - description: request + in: path + name: id + required: true + type: integer + responses: + "200": + description: OK + security: + - ApiKeyAuth: [] + summary: Search website ssl by id + tags: + - Website SSL + /websites/ssl/del: + post: + consumes: + - application/json + description: 删除网站 ssl + parameters: + - description: request + in: body + name: request + required: true + schema: + $ref: '#/definitions/request.WebsiteBatchDelReq' + responses: + "200": + description: OK + security: + - ApiKeyAuth: [] + summary: Delete website ssl + tags: + - Website SSL + x-panel-log: + BeforeFunctions: + - db: website_ssls + input_column: id + input_value: ids + isList: true + output_column: primary_domain + output_value: domain + bodyKeys: + - ids + formatEN: Delete ssl [domain] + formatZH: 删除 ssl [domain] + paramKeys: [] + /websites/ssl/download: + post: + consumes: + - application/json + description: 下载证书文件 + parameters: + - description: request + in: body + name: request + required: true + schema: + $ref: '#/definitions/request.WebsiteResourceReq' + responses: + "200": + description: OK + security: + - ApiKeyAuth: [] + summary: Download SSL file + tags: + - Website SSL + x-panel-log: + BeforeFunctions: + - db: website_ssls + input_column: id + input_value: id + isList: false + output_column: primary_domain + output_value: domain + bodyKeys: + - id + formatEN: download ssl file [domain] + formatZH: 下载证书文件 [domain] + paramKeys: [] + /websites/ssl/obtain: + post: + consumes: + - application/json + description: 申请证书 + parameters: + - description: request + in: body + name: request + required: true + schema: + $ref: '#/definitions/request.WebsiteSSLApply' + responses: + "200": + description: OK + security: + - ApiKeyAuth: [] + summary: Apply ssl + tags: + - Website SSL + x-panel-log: + BeforeFunctions: + - db: website_ssls + input_column: id + input_value: ID + isList: false + output_column: primary_domain + output_value: domain + bodyKeys: + - ID + formatEN: apply ssl [domain] + formatZH: 申请证书 [domain] + paramKeys: [] + /websites/ssl/resolve: + post: + consumes: + - application/json + description: 解析网站 ssl + parameters: + - description: request + in: body + name: request + required: true + schema: + $ref: '#/definitions/request.WebsiteDNSReq' + responses: + "200": + description: OK + schema: + items: + $ref: '#/definitions/response.WebsiteDNSRes' + type: array + security: + - ApiKeyAuth: [] + summary: Resolve website ssl + tags: + - Website SSL + /websites/ssl/search: + post: + consumes: + - application/json + description: 获取网站 ssl 列表分页 + parameters: + - description: request + in: body + name: request + required: true + schema: + $ref: '#/definitions/request.WebsiteSSLSearch' + responses: + "200": + description: OK + security: + - ApiKeyAuth: [] + summary: Page website ssl + tags: + - Website SSL + /websites/ssl/update: + post: + consumes: + - application/json + description: 更新 ssl + parameters: + - description: request + in: body + name: request + required: true + schema: + $ref: '#/definitions/request.WebsiteSSLUpdate' + responses: + "200": + description: OK + security: + - ApiKeyAuth: [] + summary: Update ssl + tags: + - Website SSL + x-panel-log: + BeforeFunctions: + - db: website_ssls + input_column: id + input_value: id + isList: false + output_column: primary_domain + output_value: domain + bodyKeys: + - id + formatEN: Update ssl config [domain] + formatZH: 更新证书设置 [domain] + paramKeys: [] + /websites/ssl/upload: + post: + consumes: + - application/json + description: 上传 ssl + parameters: + - description: request + in: body + name: request + required: true + schema: + $ref: '#/definitions/request.WebsiteSSLUpload' + responses: + "200": + description: OK + security: + - ApiKeyAuth: [] + summary: Upload ssl + tags: + - Website SSL + x-panel-log: + BeforeFunctions: [] + bodyKeys: + - type + formatEN: Upload ssl [type] + formatZH: 上传 ssl [type] + paramKeys: [] + /websites/ssl/website/:websiteId: + get: + consumes: + - application/json + description: 通过网站 id 查询 ssl + parameters: + - description: request + in: path + name: websiteId + required: true + type: integer + responses: + "200": + description: OK + security: + - ApiKeyAuth: [] + summary: Search website ssl by website id + tags: + - Website SSL + /websites/update: + post: + consumes: + - application/json + description: 更新网站 + parameters: + - description: request + in: body + name: request + required: true + schema: + $ref: '#/definitions/request.WebsiteUpdate' + responses: + "200": + description: OK + security: + - ApiKeyAuth: [] + summary: Update website + tags: + - Website + x-panel-log: + BeforeFunctions: [] + bodyKeys: + - primaryDomain + formatEN: Update website [primaryDomain] + formatZH: 更新网站 [primaryDomain] + paramKeys: [] +swagger: "2.0" diff --git a/agent/cmd/server/main.go b/agent/cmd/server/main.go new file mode 100644 index 000000000..bc99c08d1 --- /dev/null +++ b/agent/cmd/server/main.go @@ -0,0 +1,22 @@ +package main + +import ( + _ "net/http/pprof" + + _ "github.com/1Panel-dev/1Panel/agent/cmd/server/docs" + "github.com/1Panel-dev/1Panel/agent/server" +) + +// @title 1Panel +// @version 1.0 +// @description 开源Linux面板 +// @termsOfService http://swagger.io/terms/ +// @license.name Apache 2.0 +// @license.url http://www.apache.org/licenses/LICENSE-2.0.html +// @host localhost +// @BasePath /api/v1 + +//go:generate swag init -o ./docs -g main.go -d ../../backend -g ../cmd/server/main.go +func main() { + server.Start() +} diff --git a/agent/cmd/server/nginx_conf/404.html b/agent/cmd/server/nginx_conf/404.html new file mode 100644 index 000000000..d75ed7812 --- /dev/null +++ b/agent/cmd/server/nginx_conf/404.html @@ -0,0 +1,6 @@ + +404 Not Found + +

404 Not Found

+
nginx
+ \ No newline at end of file diff --git a/agent/cmd/server/nginx_conf/cache.conf b/agent/cmd/server/nginx_conf/cache.conf new file mode 100644 index 000000000..ef1285ba8 --- /dev/null +++ b/agent/cmd/server/nginx_conf/cache.conf @@ -0,0 +1,12 @@ +proxy_temp_path /www/common/proxy/proxy_temp_dir; +proxy_cache_path /www/common/proxy/proxy_cache_dir levels=1:2 keys_zone=proxy_cache_panel:20m inactive=1d max_size=5g; +client_body_buffer_size 512k; +proxy_connect_timeout 60; +proxy_read_timeout 60; +proxy_send_timeout 60; +proxy_buffer_size 32k; +proxy_buffers 4 64k; +proxy_busy_buffers_size 128k; +proxy_temp_file_write_size 128k; +proxy_next_upstream error timeout invalid_header http_500 http_503 http_404; +proxy_cache proxy_cache_panel; \ No newline at end of file diff --git a/agent/cmd/server/nginx_conf/domain404.html b/agent/cmd/server/nginx_conf/domain404.html new file mode 100644 index 000000000..d75ed7812 --- /dev/null +++ b/agent/cmd/server/nginx_conf/domain404.html @@ -0,0 +1,6 @@ + +404 Not Found + +

404 Not Found

+
nginx
+ \ No newline at end of file diff --git a/agent/cmd/server/nginx_conf/gzip.conf b/agent/cmd/server/nginx_conf/gzip.conf new file mode 100644 index 000000000..b277f1f89 --- /dev/null +++ b/agent/cmd/server/nginx_conf/gzip.conf @@ -0,0 +1,4 @@ +gzip on; +gzip_comp_level 6; +gzip_min_length 1k; +gzip_types text/plain text/css text/xml text/javascript text/x-component application/json application/javascript application/x-javascript application/xml application/xhtml+xml application/rss+xml application/atom+xml application/x-font-ttf application/vnd.ms-fontobject image/svg+xml image/x-icon font/opentype; \ No newline at end of file diff --git a/agent/cmd/server/nginx_conf/index.html b/agent/cmd/server/nginx_conf/index.html new file mode 100644 index 000000000..6c9cb573d --- /dev/null +++ b/agent/cmd/server/nginx_conf/index.html @@ -0,0 +1,35 @@ + + + + + + 恭喜,站点创建成功! + + + +
+

恭喜, 站点创建成功!

+

这是默认index.html,本页面由系统自动生成

+
+ + \ No newline at end of file diff --git a/agent/cmd/server/nginx_conf/index.php b/agent/cmd/server/nginx_conf/index.php new file mode 100644 index 000000000..5cd4e8c46 --- /dev/null +++ b/agent/cmd/server/nginx_conf/index.php @@ -0,0 +1,25 @@ +欢迎使用 PHP!'; +echo '

版本信息

'; + +echo ''; + +echo '

已安装扩展

'; +printExtensions(); + +/** + * 获取已安装扩展列表 + */ +function printExtensions() +{ + echo '
    '; + foreach (get_loaded_extensions() as $i => $name) { + echo "
  1. ", $name, '=', phpversion($name), '
  2. '; + } + echo '
'; +} \ No newline at end of file diff --git a/agent/cmd/server/nginx_conf/nginx_conf.go b/agent/cmd/server/nginx_conf/nginx_conf.go new file mode 100644 index 000000000..7b376a9b0 --- /dev/null +++ b/agent/cmd/server/nginx_conf/nginx_conf.go @@ -0,0 +1,39 @@ +package nginx_conf + +import ( + "embed" + _ "embed" +) + +//go:embed ssl.conf +var SSL []byte + +//go:embed website_default.conf +var WebsiteDefault []byte + +//go:embed index.html +var Index []byte + +//go:embed index.php +var IndexPHP []byte + +//go:embed rewrite/* +var Rewrites embed.FS + +//go:embed cache.conf +var Cache []byte + +//go:embed proxy.conf +var Proxy []byte + +//go:embed proxy_cache.conf +var ProxyCache []byte + +//go:embed 404.html +var NotFoundHTML []byte + +//go:embed domain404.html +var DomainNotFoundHTML []byte + +//go:embed stop.html +var StopHTML []byte diff --git a/agent/cmd/server/nginx_conf/proxy.conf b/agent/cmd/server/nginx_conf/proxy.conf new file mode 100644 index 000000000..d1914fb69 --- /dev/null +++ b/agent/cmd/server/nginx_conf/proxy.conf @@ -0,0 +1,13 @@ +location ^~ /test { + proxy_pass http://1panel.cloud/; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header REMOTE-HOST $remote_addr; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection "upgrade"; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_http_version 1.1; + + add_header X-Cache $upstream_cache_status; +} diff --git a/agent/cmd/server/nginx_conf/proxy_cache.conf b/agent/cmd/server/nginx_conf/proxy_cache.conf new file mode 100644 index 000000000..45172d6b6 --- /dev/null +++ b/agent/cmd/server/nginx_conf/proxy_cache.conf @@ -0,0 +1,4 @@ +proxy_ignore_headers Set-Cookie Cache-Control expires; +proxy_cache cache_one; +proxy_cache_key $host$uri$is_args$args; +proxy_cache_valid 200 304 301 302 10m; \ No newline at end of file diff --git a/agent/cmd/server/nginx_conf/proxy_no_cache.conf b/agent/cmd/server/nginx_conf/proxy_no_cache.conf new file mode 100644 index 000000000..597f25ac4 --- /dev/null +++ b/agent/cmd/server/nginx_conf/proxy_no_cache.conf @@ -0,0 +1,10 @@ +set $static_fileg 0; +if ( $uri ~* "\.(gif|png|jpg|css|js|woff|woff2)$" ) +{ + set $static_fileg 1; + expires 1m; +} +if ( $static_fileg = 0 ) +{ + add_header Cache-Control no-cache; +} diff --git a/agent/cmd/server/nginx_conf/rewrite/crmeb.conf b/agent/cmd/server/nginx_conf/rewrite/crmeb.conf new file mode 100644 index 000000000..4260774ee --- /dev/null +++ b/agent/cmd/server/nginx_conf/rewrite/crmeb.conf @@ -0,0 +1,6 @@ +location / { + if (!-e $request_filename) { + rewrite ^(.*)$ /index.php?s=/$1 last; + break; + } +} diff --git a/agent/cmd/server/nginx_conf/rewrite/dabr.conf b/agent/cmd/server/nginx_conf/rewrite/dabr.conf new file mode 100644 index 000000000..37c13132f --- /dev/null +++ b/agent/cmd/server/nginx_conf/rewrite/dabr.conf @@ -0,0 +1,5 @@ +location / { +if (!-e $request_filename) { +rewrite ^/(.*)$ /index.php?q=$1 last; +} +} \ No newline at end of file diff --git a/agent/cmd/server/nginx_conf/rewrite/dbshop.conf b/agent/cmd/server/nginx_conf/rewrite/dbshop.conf new file mode 100644 index 000000000..61d23f711 --- /dev/null +++ b/agent/cmd/server/nginx_conf/rewrite/dbshop.conf @@ -0,0 +1,7 @@ +location /{ + try_files $uri $uri/ /index.php$is_args$args; +} + +location ~ \.htaccess{ + deny all; +} \ No newline at end of file diff --git a/agent/cmd/server/nginx_conf/rewrite/dedecms.conf b/agent/cmd/server/nginx_conf/rewrite/dedecms.conf new file mode 100644 index 000000000..6e110cba5 --- /dev/null +++ b/agent/cmd/server/nginx_conf/rewrite/dedecms.conf @@ -0,0 +1,10 @@ +rewrite "^/list-([0-9]+)\.html$" /plus/list.php?tid=$1 last; +rewrite "^/list-([0-9]+)-([0-9]+)-([0-9]+)\.html$" /plus/list.php?tid=$1&totalresult=$2&PageNo=$3 last; +rewrite "^/view-([0-9]+)-1\.html$" /plus/view.php?arcID=$1 last; +rewrite "^/view-([0-9]+)-([0-9]+)\.html$" /plus/view.php?aid=$1&pageno=$2 last; +rewrite "^/plus/list-([0-9]+)\.html$" /plus/list.php?tid=$1 last; +rewrite "^/plus/list-([0-9]+)-([0-9]+)-([0-9]+)\.html$" /plus/list.php?tid=$1&totalresult=$2&PageNo=$3 last; +rewrite "^/plus/view-([0-9]+)-1\.html$" /plus/view.php?arcID=$1 last; +rewrite "^/plus/view-([0-9]+)-([0-9]+)\.html$" /plus/view.php?aid=$1&pageno=$2 last; +rewrite "^/tags.html$" /tags.php last; +rewrite "^/tag-([0-9]+)-([0-9]+)\.html$" /tags.php?/$1/$2/ last; diff --git a/agent/cmd/server/nginx_conf/rewrite/default.conf b/agent/cmd/server/nginx_conf/rewrite/default.conf new file mode 100644 index 000000000..e69de29bb diff --git a/agent/cmd/server/nginx_conf/rewrite/discuz.conf b/agent/cmd/server/nginx_conf/rewrite/discuz.conf new file mode 100644 index 000000000..578da7653 --- /dev/null +++ b/agent/cmd/server/nginx_conf/rewrite/discuz.conf @@ -0,0 +1,7 @@ +location / { + rewrite ^/archiver/((fid|tid)-[\w\-]+\.html)$ /archiver/index.php?$1 last; + rewrite ^/forum-([0-9]+)-([0-9]+)\.html$ /forumdisplay.php?fid=$1&page=$2 last; + rewrite ^/thread-([0-9]+)-([0-9]+)-([0-9]+)\.html$ /viewthread.php?tid=$1&extra=page%3D$3&page=$2 last; + rewrite ^/space-(username|uid)-(.+)\.html$ /space.php?$1=$2 last; + rewrite ^/tag-(.+)\.html$ /tag.php?name=$1 last; + } \ No newline at end of file diff --git a/agent/cmd/server/nginx_conf/rewrite/discuzx.conf b/agent/cmd/server/nginx_conf/rewrite/discuzx.conf new file mode 100644 index 000000000..8058495a2 --- /dev/null +++ b/agent/cmd/server/nginx_conf/rewrite/discuzx.conf @@ -0,0 +1,12 @@ +rewrite ^([^\.]*)/topic-(.+)\.html$ $1/portal.php?mod=topic&topic=$2 last; +rewrite ^([^\.]*)/article-([0-9]+)-([0-9]+)\.html$ $1/portal.php?mod=view&aid=$2&page=$3 last; +rewrite ^([^\.]*)/forum-(\w+)-([0-9]+)\.html$ $1/forum.php?mod=forumdisplay&fid=$2&page=$3 last; +rewrite ^([^\.]*)/thread-([0-9]+)-([0-9]+)-([0-9]+)\.html$ $1/forum.php?mod=viewthread&tid=$2&extra=page%3D$4&page=$3 last; +rewrite ^([^\.]*)/group-([0-9]+)-([0-9]+)\.html$ $1/forum.php?mod=group&fid=$2&page=$3 last; +rewrite ^([^\.]*)/space-(username|uid)-(.+)\.html$ $1/home.php?mod=space&$2=$3 last; +rewrite ^([^\.]*)/blog-([0-9]+)-([0-9]+)\.html$ $1/home.php?mod=space&uid=$2&do=blog&id=$3 last; +rewrite ^([^\.]*)/(fid|tid)-([0-9]+)\.html$ $1/index.php?action=$2&value=$3 last; +rewrite ^([^\.]*)/([a-z]+[a-z0-9_]*)-([a-z0-9_\-]+)\.html$ $1/plugin.php?id=$2:$3 last; +if (!-e $request_filename) { + return 404; +} \ No newline at end of file diff --git a/agent/cmd/server/nginx_conf/rewrite/discuzx2.conf b/agent/cmd/server/nginx_conf/rewrite/discuzx2.conf new file mode 100644 index 000000000..61059e21e --- /dev/null +++ b/agent/cmd/server/nginx_conf/rewrite/discuzx2.conf @@ -0,0 +1,14 @@ +location /bbs/ { + rewrite ^([^\.]*)/topic-(.+)\.html$ $1/portal.php?mod=topic&topic=$2 last; + rewrite ^([^\.]*)/article-([0-9]+)-([0-9]+)\.html$ $1/portal.php?mod=view&aid=$2&page=$3 last; + rewrite ^([^\.]*)/forum-(\w+)-([0-9]+)\.html$ $1/forum.php?mod=forumdisplay&fid=$2&page=$3 last; + rewrite ^([^\.]*)/thread-([0-9]+)-([0-9]+)-([0-9]+)\.html$ $1/forum.php?mod=viewthread&tid=$2&extra=page%3D$4&page=$3 last; + rewrite ^([^\.]*)/group-([0-9]+)-([0-9]+)\.html$ $1/forum.php?mod=group&fid=$2&page=$3 last; + rewrite ^([^\.]*)/space-(username|uid)-(.+)\.html$ $1/home.php?mod=space&$2=$3 last; + rewrite ^([^\.]*)/blog-([0-9]+)-([0-9]+)\.html$ $1/home.php?mod=space&uid=$2&do=blog&id=$3 last; + rewrite ^([^\.]*)/(fid|tid)-([0-9]+)\.html$ $1/index.php?action=$2&value=$3 last; + rewrite ^([^\.]*)/([a-z]+[a-z0-9_]*)-([a-z0-9_\-]+)\.html$ $1/plugin.php?id=$2:$3 last; + if (!-e $request_filename) { + return 404; + } +} \ No newline at end of file diff --git a/agent/cmd/server/nginx_conf/rewrite/discuzx3.conf b/agent/cmd/server/nginx_conf/rewrite/discuzx3.conf new file mode 100644 index 000000000..661889734 --- /dev/null +++ b/agent/cmd/server/nginx_conf/rewrite/discuzx3.conf @@ -0,0 +1,15 @@ +location / { + rewrite ^([^\.]*)/topic-(.+)\.html$ $1/portal.php?mod=topic&topic=$2 last; + rewrite ^([^\.]*)/article-([0-9]+)-([0-9]+)\.html$ $1/portal.php?mod=view&aid=$2&page=$3 last; + rewrite ^([^\.]*)/forum-(\w+)-([0-9]+)\.html$ $1/forum.php?mod=forumdisplay&fid=$2&page=$3 last; + rewrite ^([^\.]*)/thread-([0-9]+)-([0-9]+)-([0-9]+)\.html$ $1/forum.php?mod=viewthread&tid=$2&extra=page%3D$4&page=$3 last; + rewrite ^([^\.]*)/group-([0-9]+)-([0-9]+)\.html$ $1/forum.php?mod=group&fid=$2&page=$3 last; + rewrite ^([^\.]*)/space-(username|uid)-(.+)\.html$ $1/home.php?mod=space&$2=$3 last; + rewrite ^([^\.]*)/blog-([0-9]+)-([0-9]+)\.html$ $1/home.php?mod=space&uid=$2&do=blog&id=$3 last; + rewrite ^([^\.]*)/(fid|tid)-([0-9]+)\.html$ $1/index.php?action=$2&value=$3 last; + rewrite ^([^\.]*)/([a-z]+[a-z0-9_]*)-([a-z0-9_\-]+)\.html$ $1/plugin.php?id=$2:$3 last; + if (!-e $request_filename) { + return 404; + } +} + diff --git a/agent/cmd/server/nginx_conf/rewrite/drupal.conf b/agent/cmd/server/nginx_conf/rewrite/drupal.conf new file mode 100644 index 000000000..460b7791d --- /dev/null +++ b/agent/cmd/server/nginx_conf/rewrite/drupal.conf @@ -0,0 +1,3 @@ +if (!-e $request_filename) { + rewrite ^/(.*)$ /index.php?q=$1 last; + } \ No newline at end of file diff --git a/agent/cmd/server/nginx_conf/rewrite/ecshop.conf b/agent/cmd/server/nginx_conf/rewrite/ecshop.conf new file mode 100644 index 000000000..3574daa73 --- /dev/null +++ b/agent/cmd/server/nginx_conf/rewrite/ecshop.conf @@ -0,0 +1,32 @@ +if (!-e $request_filename) +{ +rewrite "^/index\.html" /index.php last; +rewrite "^/category$" /index.php last; +rewrite "^/feed-c([0-9]+)\.xml$" /feed.php?cat=$1 last; +rewrite "^/feed-b([0-9]+)\.xml$" /feed.php?brand=$1 last; +rewrite "^/feed\.xml$" /feed.php last; +rewrite "^/category-([0-9]+)-b([0-9]+)-min([0-9]+)-max([0-9]+)-attr([^-]*)-([0-9]+)-(.+)-([a-zA-Z]+)(.*)\.html$" /category.php?id=$1&brand=$2&price_min=$3&price_max=$4&filter_attr=$5&page=$6&sort=$7&order=$8 last; +rewrite "^/category-([0-9]+)-b([0-9]+)-min([0-9]+)-max([0-9]+)-attr([^-]*)(.*)\.html$" /category.php?id=$1&brand=$2&price_min=$3&price_max=$4&filter_attr=$5 last; +rewrite "^/category-([0-9]+)-b([0-9]+)-([0-9]+)-(.+)-([a-zA-Z]+)(.*)\.html$" /category.php?id=$1&brand=$2&page=$3&sort=$4&order=$5 last; +rewrite "^/category-([0-9]+)-b([0-9]+)-([0-9]+)(.*)\.html$" /category.php?id=$1&brand=$2&page=$3 last; +rewrite "^/category-([0-9]+)-b([0-9]+)(.*)\.html$" /category.php?id=$1&brand=$2 last; +rewrite "^/category-([0-9]+)(.*)\.html$" /category.php?id=$1 last; +rewrite "^/goods-([0-9]+)(.*)\.html" /goods.php?id=$1 last; +rewrite "^/article_cat-([0-9]+)-([0-9]+)-(.+)-([a-zA-Z]+)(.*)\.html$" /article_cat.php?id=$1&page=$2&sort=$3&order=$4 last; +rewrite "^/article_cat-([0-9]+)-([0-9]+)(.*)\.html$" /article_cat.php?id=$1&page=$2 last; +rewrite "^/article_cat-([0-9]+)(.*)\.html$" /article_cat.php?id=$1 last; +rewrite "^/article-([0-9]+)(.*)\.html$" /article.php?id=$1 last; +rewrite "^/brand-([0-9]+)-c([0-9]+)-([0-9]+)-(.+)-([a-zA-Z]+)\.html" /brand.php?id=$1&cat=$2&page=$3&sort=$4&order=$5 last; +rewrite "^/brand-([0-9]+)-c([0-9]+)-([0-9]+)(.*)\.html" /brand.php?id=$1&cat=$2&page=$3 last; +rewrite "^/brand-([0-9]+)-c([0-9]+)(.*)\.html" /brand.php?id=$1&cat=$2 last; +rewrite "^/brand-([0-9]+)(.*)\.html" /brand.php?id=$1 last; +rewrite "^/tag-(.*)\.html" /search.php?keywords=$1 last; +rewrite "^/snatch-([0-9]+)\.html$" /snatch.php?id=$1 last; +rewrite "^/group_buy-([0-9]+)\.html$" /group_buy.php?act=view&id=$1 last; +rewrite "^/auction-([0-9]+)\.html$" /auction.php?act=view&id=$1 last; +rewrite "^/exchange-id([0-9]+)(.*)\.html$" /exchange.php?id=$1&act=view last; +rewrite "^/exchange-([0-9]+)-min([0-9]+)-max([0-9]+)-([0-9]+)-(.+)-([a-zA-Z]+)(.*)\.html$" /exchange.php?cat_id=$1&integral_min=$2&integral_max=$3&page=$4&sort=$5&order=$6 last; +rewrite ^/exchange-([0-9]+)-([0-9]+)-(.+)-([a-zA-Z]+)(.*)\.html$" /exchange.php?cat_id=$1&page=$2&sort=$3&order=$4 last; +rewrite "^/exchange-([0-9]+)-([0-9]+)(.*)\.html$" /exchange.php?cat_id=$1&page=$2 last; +rewrite "^/exchange-([0-9]+)(.*)\.html$" /exchange.php?cat_id=$1 last; +} \ No newline at end of file diff --git a/agent/cmd/server/nginx_conf/rewrite/edusoho.conf b/agent/cmd/server/nginx_conf/rewrite/edusoho.conf new file mode 100644 index 000000000..b0099dcff --- /dev/null +++ b/agent/cmd/server/nginx_conf/rewrite/edusoho.conf @@ -0,0 +1,8 @@ +location / { + index app.php; + try_files $uri @rewriteapp; +} + +location @rewriteapp { + rewrite ^(.*)$ /app.php/$1 last; +} \ No newline at end of file diff --git a/agent/cmd/server/nginx_conf/rewrite/emlog.conf b/agent/cmd/server/nginx_conf/rewrite/emlog.conf new file mode 100644 index 000000000..e122a854d --- /dev/null +++ b/agent/cmd/server/nginx_conf/rewrite/emlog.conf @@ -0,0 +1,7 @@ +location / { +index index.php index.html; + if (!-e $request_filename) + { + rewrite ^/(.*)$ /index.php last; + } +} \ No newline at end of file diff --git a/agent/cmd/server/nginx_conf/rewrite/empirecms.conf b/agent/cmd/server/nginx_conf/rewrite/empirecms.conf new file mode 100644 index 000000000..c68b92d85 --- /dev/null +++ b/agent/cmd/server/nginx_conf/rewrite/empirecms.conf @@ -0,0 +1,8 @@ +rewrite ^([^\.]*)/listinfo-(.+?)-(.+?)\.html$ $1/e/action/ListInfo/index.php?classid=$2&page=$3 last; +rewrite ^([^\.]*)/showinfo-(.+?)-(.+?)-(.+?)\.html$ $1/e/action/ShowInfo.php?classid=$2&id=$3&page=$4 last; +rewrite ^([^\.]*)/infotype-(.+?)-(.+?)\.html$ $1/e/action/InfoType/index.php?ttid=$2&page=$3 last; +rewrite ^([^\.]*)/tags-(.+?)-(.+?)\.html$ $1/e/tags/index.php?tagname=$2&page=$3 last; +rewrite ^([^\.]*)/comment-(.+?)-(.+?)-(.+?)-(.+?)-(.+?)-(.+?)\.html$ $1/e/pl/index\.php\?doaction=$2&classid=$3&id=$4&page=$5&myorder=$6&tempid=$7 last; +if (!-e $request_filename) { + return 404; +} \ No newline at end of file diff --git a/agent/cmd/server/nginx_conf/rewrite/laravel5.conf b/agent/cmd/server/nginx_conf/rewrite/laravel5.conf new file mode 100644 index 000000000..da4c67474 --- /dev/null +++ b/agent/cmd/server/nginx_conf/rewrite/laravel5.conf @@ -0,0 +1,3 @@ +location / { + try_files $uri $uri/ /index.php?$query_string; +} \ No newline at end of file diff --git a/agent/cmd/server/nginx_conf/rewrite/maccms.conf b/agent/cmd/server/nginx_conf/rewrite/maccms.conf new file mode 100644 index 000000000..b08325169 --- /dev/null +++ b/agent/cmd/server/nginx_conf/rewrite/maccms.conf @@ -0,0 +1,13 @@ +if (!-e $request_filename) { + rewrite ^/index.php(.*)$ /index.php?s=$1 break; + # MacCMS要求强制修改后台文件名称 所以需要手动修改下方这条重写规则 将admin修改为你修改后的文件名即可 + rewrite ^/admin.php(.*)$ /admin.php?s=$1 break; + rewrite ^/api.php(.*)$ /api.php?s=$1 break; + rewrite ^/(.*)$ /index.php?s=$1 break; + rewrite ^/vod-(.*)$ /index.php?m=vod-$1 break; + rewrite ^/art-(.*)$ /index.php?m=art-$1 break; + rewrite ^/gbook-(.*)$ /index.php?m=gbook-$1 break; + rewrite ^/label-(.*)$ /index.php?m=label-$1 break; + rewrite ^/map-(.*)$ /index.php?m=map-$1 break; + } +try_files $uri $uri/ /index.php?$query_string; \ No newline at end of file diff --git a/agent/cmd/server/nginx_conf/rewrite/mvc.conf b/agent/cmd/server/nginx_conf/rewrite/mvc.conf new file mode 100644 index 000000000..bf906257b --- /dev/null +++ b/agent/cmd/server/nginx_conf/rewrite/mvc.conf @@ -0,0 +1,6 @@ +location /{ + if (!-e $request_filename) { + rewrite ^(.*)$ /index.php/$1 last; + break; + } +} diff --git a/agent/cmd/server/nginx_conf/rewrite/niushop.conf b/agent/cmd/server/nginx_conf/rewrite/niushop.conf new file mode 100644 index 000000000..c32c40c71 --- /dev/null +++ b/agent/cmd/server/nginx_conf/rewrite/niushop.conf @@ -0,0 +1,6 @@ +location / { + if (!-e $request_filename) { + rewrite ^(.*)$ /index.php?s=$1 last; + break; + } +} \ No newline at end of file diff --git a/agent/cmd/server/nginx_conf/rewrite/phpcms.conf b/agent/cmd/server/nginx_conf/rewrite/phpcms.conf new file mode 100644 index 000000000..a6e0df346 --- /dev/null +++ b/agent/cmd/server/nginx_conf/rewrite/phpcms.conf @@ -0,0 +1,9 @@ +location / { + ###以下为PHPCMS 伪静态化rewrite法则 + rewrite ^(.*)show-([0-9]+)-([0-9]+)\.html$ $1/show.php?itemid=$2&page=$3; + rewrite ^(.*)list-([0-9]+)-([0-9]+)\.html$ $1/list.php?catid=$2&page=$3; + rewrite ^(.*)show-([0-9]+)\.html$ $1/show.php?specialid=$2; + ####以下为PHPWind 伪静态化rewrite法则 + rewrite ^(.*)-htm-(.*)$ $1.php?$2 last; + rewrite ^(.*)/simple/([a-z0-9\_]+\.html)$ $1/simple/index.php?$2 last; +} \ No newline at end of file diff --git a/agent/cmd/server/nginx_conf/rewrite/phpwind.conf b/agent/cmd/server/nginx_conf/rewrite/phpwind.conf new file mode 100644 index 000000000..388af90d3 --- /dev/null +++ b/agent/cmd/server/nginx_conf/rewrite/phpwind.conf @@ -0,0 +1,4 @@ +location / { + rewrite ^(.*)-htm-(.*)$ $1.php?$2 last; + rewrite ^(.*)/simple/([a-z0-9\_]+\.html)$ $1/simple/index.php?$2 last; + } \ No newline at end of file diff --git a/agent/cmd/server/nginx_conf/rewrite/sablog.conf b/agent/cmd/server/nginx_conf/rewrite/sablog.conf new file mode 100644 index 000000000..fa4f00c49 --- /dev/null +++ b/agent/cmd/server/nginx_conf/rewrite/sablog.conf @@ -0,0 +1,16 @@ +location / { + rewrite "^/date/([0-9]{6})/?([0-9]+)?/?$" /index.php?action=article&setdate=$1&page=$2 last; + rewrite ^/page/([0-9]+)?/?$ /index.php?action=article&page=$1 last; + rewrite ^/category/([0-9]+)/?([0-9]+)?/?$ /index.php?action=article&cid=$1&page=$2 last; + rewrite ^/category/([^/]+)/?([0-9]+)?/?$ /index.php?action=article&curl=$1&page=$2 last; + rewrite ^/(archives|search|article|links)/?$ /index.php?action=$1 last; + rewrite ^/(comments|tagslist|trackbacks|article)/?([0-9]+)?/?$ /index.php?action=$1&page=$2 last; + rewrite ^/tag/([^/]+)/?([0-9]+)?/?$ /index.php?action=article&item=$1&page=$2 last; + rewrite ^/archives/([0-9]+)/?([0-9]+)?/?$ /index.php?action=show&id=$1&page=$2 last; + rewrite ^/rss/([0-9]+)?/?$ /rss.php?cid=$1 last; + rewrite ^/rss/([^/]+)/?$ /rss.php?url=$1 last; + rewrite ^/uid/([0-9]+)/?([0-9]+)?/?$ /index.php?action=article&uid=$1&page=$2 last; + rewrite ^/user/([^/]+)/?([0-9]+)?/?$ /index.php?action=article&user=$1&page=$2 last; + rewrite sitemap.xml sitemap.php last; + rewrite ^(.*)/([0-9a-zA-Z\-\_]+)/?([0-9]+)?/?$ $1/index.php?action=show&alias=$2&page=$3 last; +} \ No newline at end of file diff --git a/agent/cmd/server/nginx_conf/rewrite/seacms.conf b/agent/cmd/server/nginx_conf/rewrite/seacms.conf new file mode 100644 index 000000000..0dc6f3612 --- /dev/null +++ b/agent/cmd/server/nginx_conf/rewrite/seacms.conf @@ -0,0 +1,11 @@ +location / { + rewrite ^/frim/index(.+?)\.html$ /list/index.php?$1 last; + rewrite ^/movie/index(.+?)\.html$ /detail/index.php?$1 last; + rewrite ^/play/([0-9]+)-([0-9]+)-([0-9]+)\.html$ /video/index.php?$1-$2-$3 last; + rewrite ^/topic/index(.+?)\.html$ /topic/index.php?$1 last; + rewrite ^/topiclist/index(.+?).html$ /topiclist/index.php?$1 last; + rewrite ^/index\.html$ index.php permanent; + rewrite ^/news\.html$ news/ permanent; + rewrite ^/part/index(.+?)\.html$ /articlelist/index.php?$1 last; + rewrite ^/article/index(.+?)\.html$ /article/index.php?$1 last; +} \ No newline at end of file diff --git a/agent/cmd/server/nginx_conf/rewrite/shopex.conf b/agent/cmd/server/nginx_conf/rewrite/shopex.conf new file mode 100644 index 000000000..f57463c1a --- /dev/null +++ b/agent/cmd/server/nginx_conf/rewrite/shopex.conf @@ -0,0 +1,5 @@ +location / { +if (!-e $request_filename) { +rewrite ^/(.+\.(html|xml|json|htm|php|jsp|asp|shtml))$ /index.php?$1 last; +} +} \ No newline at end of file diff --git a/agent/cmd/server/nginx_conf/rewrite/shopwind.conf b/agent/cmd/server/nginx_conf/rewrite/shopwind.conf new file mode 100644 index 000000000..0edf086c3 --- /dev/null +++ b/agent/cmd/server/nginx_conf/rewrite/shopwind.conf @@ -0,0 +1,14 @@ +location / { + #Redirect everything that isn't a real file to index.php + try_files $uri $uri/ /index.php$is_args$args; +} +#If you want a single domain name at the front and back ends +location /admin { + try_files $uri $uri/ /admin/index.php$is_args$args; +} +location /mobile { + try_files $uri $uri/ /mobile/index.php$is_args$args; +} +location /api { + try_files $uri $uri/ /api/index.php$is_args$args; +} \ No newline at end of file diff --git a/agent/cmd/server/nginx_conf/rewrite/thinkphp.conf b/agent/cmd/server/nginx_conf/rewrite/thinkphp.conf new file mode 100644 index 000000000..216b1dc23 --- /dev/null +++ b/agent/cmd/server/nginx_conf/rewrite/thinkphp.conf @@ -0,0 +1,8 @@ +location ~* (runtime|application)/ { + return 403; +} +location / { + if (!-e $request_filename){ + rewrite ^(.*)$ /index.php?s=$1 last; break; + } +} \ No newline at end of file diff --git a/agent/cmd/server/nginx_conf/rewrite/typecho.conf b/agent/cmd/server/nginx_conf/rewrite/typecho.conf new file mode 100644 index 000000000..dae6ba9cc --- /dev/null +++ b/agent/cmd/server/nginx_conf/rewrite/typecho.conf @@ -0,0 +1,3 @@ + if (!-e $request_filename) { + rewrite ^(.*)$ /index.php$1 last; + } diff --git a/agent/cmd/server/nginx_conf/rewrite/typecho2.conf b/agent/cmd/server/nginx_conf/rewrite/typecho2.conf new file mode 100644 index 000000000..22397d847 --- /dev/null +++ b/agent/cmd/server/nginx_conf/rewrite/typecho2.conf @@ -0,0 +1,5 @@ +location /typecho/ { + if (!-e $request_filename) { + rewrite ^(.*)$ /typecho/index.php$1 last; + } +} diff --git a/agent/cmd/server/nginx_conf/rewrite/wordpress.conf b/agent/cmd/server/nginx_conf/rewrite/wordpress.conf new file mode 100644 index 000000000..9fe73c1eb --- /dev/null +++ b/agent/cmd/server/nginx_conf/rewrite/wordpress.conf @@ -0,0 +1,6 @@ +location / +{ + try_files $uri $uri/ /index.php?$args; +} + +rewrite /wp-admin$ $scheme://$host$uri/ permanent; \ No newline at end of file diff --git a/agent/cmd/server/nginx_conf/rewrite/wp2.conf b/agent/cmd/server/nginx_conf/rewrite/wp2.conf new file mode 100644 index 000000000..0e5fbaede --- /dev/null +++ b/agent/cmd/server/nginx_conf/rewrite/wp2.conf @@ -0,0 +1,6 @@ +rewrite ^.*/files/(.*)$ /wp-includes/ms-files.php?file=$1 last; +if (!-e $request_filename){ + rewrite ^.+?(/wp-.*) $1 last; + rewrite ^.+?(/.*\.php)$ $1 last; + rewrite ^ /index.php last; +} \ No newline at end of file diff --git a/agent/cmd/server/nginx_conf/rewrite/yii2.conf b/agent/cmd/server/nginx_conf/rewrite/yii2.conf new file mode 100644 index 000000000..103ebdc7d --- /dev/null +++ b/agent/cmd/server/nginx_conf/rewrite/yii2.conf @@ -0,0 +1,3 @@ +location / { + try_files $uri $uri/ /index.php$is_args$args; +} \ No newline at end of file diff --git a/agent/cmd/server/nginx_conf/rewrite/zblog.conf b/agent/cmd/server/nginx_conf/rewrite/zblog.conf new file mode 100644 index 000000000..5d2de2b79 --- /dev/null +++ b/agent/cmd/server/nginx_conf/rewrite/zblog.conf @@ -0,0 +1,9 @@ +if (-f $request_filename/index.html){ + rewrite (.*) $1/index.html break; +} +if (-f $request_filename/index.php){ + rewrite (.*) $1/index.php; +} +if (!-f $request_filename){ + rewrite (.*) /index.php; +} \ No newline at end of file diff --git a/agent/cmd/server/nginx_conf/ssl.conf b/agent/cmd/server/nginx_conf/ssl.conf new file mode 100644 index 000000000..908c52fb4 --- /dev/null +++ b/agent/cmd/server/nginx_conf/ssl.conf @@ -0,0 +1,9 @@ +ssl_certificate /www/server/panel/vhost/cert/1panel.cloud/fullchain.pem; +ssl_certificate_key /www/server/panel/vhost/cert/1panel.cloud/privkey.pem; +ssl_protocols TLSv1.1 TLSv1.2 TLSv1.3; +ssl_ciphers ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384:DHE-RSA-AES128-GCM-SHA256:ECDHE-RSA-AES256-SHA384:ECDHE-RSA-AES128-SHA256:!aNULL:!eNULL:!EXPORT:!DSS:!DES:!RC4:!3DES:!MD5:!PSK:!KRB5:!SRP:!CAMELLIA:!SEED; +ssl_prefer_server_ciphers on; +ssl_session_cache shared:SSL:10m; +ssl_session_timeout 10m; +error_page 497 https://$host$request_uri; +proxy_set_header X-Forwarded-Proto https; \ No newline at end of file diff --git a/agent/cmd/server/nginx_conf/stop.html b/agent/cmd/server/nginx_conf/stop.html new file mode 100644 index 000000000..a38fa64b6 --- /dev/null +++ b/agent/cmd/server/nginx_conf/stop.html @@ -0,0 +1,33 @@ + + + + + 抱歉,站点已暂停 + + + + +
+

抱歉!该站点已经被管理员停止运行,请联系管理员了解详情!

+
+ + \ No newline at end of file diff --git a/agent/cmd/server/nginx_conf/website_default.conf b/agent/cmd/server/nginx_conf/website_default.conf new file mode 100644 index 000000000..57c439312 --- /dev/null +++ b/agent/cmd/server/nginx_conf/website_default.conf @@ -0,0 +1,23 @@ +server { + listen 80; + server_name ko.wp-1.com; + + index index.php index.html index.htm default.php default.htm default.html; + + proxy_set_header Host $host; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Host $server_name; + proxy_set_header X-Real-IP $remote_addr; + proxy_http_version 1.1; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection "upgrade"; + + + access_log /www/sites/domain/log/access.log main; + error_log /www/sites/domain/log/error.log; + + location ^~ /.well-known/acme-challenge { + allow all; + root /usr/share/nginx/html; + } +} diff --git a/agent/cmd/server/qqwry/qqwey.go b/agent/cmd/server/qqwry/qqwey.go new file mode 100644 index 000000000..94ee4abb8 --- /dev/null +++ b/agent/cmd/server/qqwry/qqwey.go @@ -0,0 +1,6 @@ +package qqwry + +import _ "embed" + +//go:embed qqwry.dat +var QQwryByte []byte diff --git a/agent/cmd/server/qqwry/qqwry.dat b/agent/cmd/server/qqwry/qqwry.dat new file mode 100644 index 000000000..0752ec831 Binary files /dev/null and b/agent/cmd/server/qqwry/qqwry.dat differ diff --git a/agent/configs/config.go b/agent/configs/config.go new file mode 100644 index 000000000..7d8116b68 --- /dev/null +++ b/agent/configs/config.go @@ -0,0 +1,6 @@ +package configs + +type ServerConfig struct { + System System `mapstructure:"system"` + LogConfig LogConfig `mapstructure:"log"` +} diff --git a/agent/configs/log.go b/agent/configs/log.go new file mode 100644 index 000000000..8fe60a91d --- /dev/null +++ b/agent/configs/log.go @@ -0,0 +1,9 @@ +package configs + +type LogConfig struct { + Level string `mapstructure:"level"` + TimeZone string `mapstructure:"timeZone"` + LogName string `mapstructure:"log_name"` + LogSuffix string `mapstructure:"log_suffix"` + MaxBackup int `mapstructure:"max_backup"` +} diff --git a/agent/configs/system.go b/agent/configs/system.go new file mode 100644 index 000000000..d6e7c87c9 --- /dev/null +++ b/agent/configs/system.go @@ -0,0 +1,20 @@ +package configs + +type System struct { + DbFile string `mapstructure:"db_file"` + DbPath string `mapstructure:"db_path"` + LogPath string `mapstructure:"log_path"` + DataDir string `mapstructure:"data_dir"` + TmpDir string `mapstructure:"tmp_dir"` + Cache string `mapstructure:"cache"` + Backup string `mapstructure:"backup"` + EncryptKey string `mapstructure:"encrypt_key"` + BaseDir string `mapstructure:"base_dir"` + Mode string `mapstructure:"mode"` + RepoUrl string `mapstructure:"repo_url"` + Version string `mapstructure:"version"` + IsDemo bool `mapstructure:"is_demo"` + AppRepo string `mapstructure:"app_repo"` + OneDriveID string `mapstructure:"one_drive_id"` + OneDriveSc string `mapstructure:"one_drive_sc"` +} diff --git a/agent/constant/app.go b/agent/constant/app.go new file mode 100644 index 000000000..9a6f5c825 --- /dev/null +++ b/agent/constant/app.go @@ -0,0 +1,54 @@ +package constant + +const ( + Running = "Running" + UnHealthy = "UnHealthy" + Error = "Error" + Stopped = "Stopped" + Installing = "Installing" + DownloadErr = "DownloadErr" + Upgrading = "Upgrading" + UpgradeErr = "UpgradeErr" + Rebuilding = "Rebuilding" + Syncing = "Syncing" + SyncSuccess = "SyncSuccess" + Paused = "Paused" + UpErr = "UpErr" + + ContainerPrefix = "1Panel-" + + AppNormal = "Normal" + AppTakeDown = "TakeDown" + + AppOpenresty = "openresty" + AppMysql = "mysql" + AppMariaDB = "mariadb" + AppPostgresql = "postgresql" + AppRedis = "redis" + AppPostgres = "postgres" + AppMongodb = "mongodb" + AppMemcached = "memcached" + + AppResourceLocal = "local" + AppResourceRemote = "remote" + + CPUS = "CPUS" + MemoryLimit = "MEMORY_LIMIT" + HostIP = "HOST_IP" + ContainerName = "CONTAINER_NAME" +) + +type AppOperate string + +var ( + Start AppOperate = "start" + Stop AppOperate = "stop" + Restart AppOperate = "restart" + Delete AppOperate = "delete" + Sync AppOperate = "sync" + Backup AppOperate = "backup" + Update AppOperate = "update" + Rebuild AppOperate = "rebuild" + Upgrade AppOperate = "upgrade" + Reload AppOperate = "reload" +) diff --git a/agent/constant/backup.go b/agent/constant/backup.go new file mode 100644 index 000000000..37baa994e --- /dev/null +++ b/agent/constant/backup.go @@ -0,0 +1,18 @@ +package constant + +const ( + Valid = "VALID" + DisConnect = "DISCONNECT" + VerifyFailed = "VERIFYFAILED" + S3 = "S3" + OSS = "OSS" + Sftp = "SFTP" + OneDrive = "OneDrive" + MinIo = "MINIO" + Cos = "COS" + Kodo = "KODO" + WebDAV = "WebDAV" + Local = "LOCAL" + + OneDriveRedirectURI = "http://localhost/login/authorized" +) diff --git a/agent/constant/common.go b/agent/constant/common.go new file mode 100644 index 000000000..cca64e21a --- /dev/null +++ b/agent/constant/common.go @@ -0,0 +1,25 @@ +package constant + +type DBContext string + +const ( + DB DBContext = "db" + + SystemRestart = "systemRestart" + + TypeWebsite = "website" + TypePhp = "php" + TypeSSL = "ssl" + TypeSystem = "system" +) + +const ( + TimeOut5s = 5 + TimeOut20s = 20 + TimeOut5m = 300 + + DateLayout = "2006-01-02" // or use time.DateOnly while go version >= 1.20 + DefaultDate = "1970-01-01" + DateTimeLayout = "2006-01-02 15:04:05" // or use time.DateTime while go version >= 1.20 + DateTimeSlimLayout = "20060102150405" +) diff --git a/agent/constant/container.go b/agent/constant/container.go new file mode 100644 index 000000000..98853664c --- /dev/null +++ b/agent/constant/container.go @@ -0,0 +1,18 @@ +package constant + +const ( + ContainerOpStart = "start" + ContainerOpStop = "stop" + ContainerOpRestart = "restart" + ContainerOpKill = "kill" + ContainerOpPause = "pause" + ContainerOpUnpause = "unpause" + ContainerOpRename = "rename" + ContainerOpRemove = "remove" + + ComposeOpStop = "stop" + ComposeOpRestart = "restart" + ComposeOpRemove = "remove" + + DaemonJsonPath = "/etc/docker/daemon.json" +) diff --git a/agent/constant/dir.go b/agent/constant/dir.go new file mode 100644 index 000000000..7cd6e1504 --- /dev/null +++ b/agent/constant/dir.go @@ -0,0 +1,21 @@ +package constant + +import ( + "path" + + "github.com/1Panel-dev/1Panel/agent/global" +) + +var ( + DataDir = global.CONF.System.DataDir + ResourceDir = path.Join(DataDir, "resource") + AppResourceDir = path.Join(ResourceDir, "apps") + AppInstallDir = path.Join(DataDir, "apps") + LocalAppResourceDir = path.Join(AppResourceDir, "local") + LocalAppInstallDir = path.Join(AppInstallDir, "local") + RemoteAppResourceDir = path.Join(AppResourceDir, "remote") + RuntimeDir = path.Join(DataDir, "runtime") + RecycleBinDir = "/.1panel_clash" + SSLLogDir = path.Join(global.CONF.System.DataDir, "log", "ssl") + LogDir = path.Join(global.CONF.System.DataDir, "log") +) diff --git a/agent/constant/errs.go b/agent/constant/errs.go new file mode 100644 index 000000000..411387d3d --- /dev/null +++ b/agent/constant/errs.go @@ -0,0 +1,164 @@ +package constant + +import ( + "errors" +) + +const ( + CodeSuccess = 200 + CodeErrBadRequest = 400 + CodeErrUnauthorized = 401 + CodeErrNotFound = 404 + CodeAuth = 406 + CodeGlobalLoading = 407 + CodeErrInternalServer = 500 + + CodeErrIP = 310 + CodeErrDomain = 311 + CodeErrEntrance = 312 + CodePasswordExpired = 313 + + CodeErrXpack = 410 +) + +// internal +var ( + ErrCaptchaCode = errors.New("ErrCaptchaCode") + ErrAuth = errors.New("ErrAuth") + ErrRecordExist = errors.New("ErrRecordExist") + ErrRecordNotFound = errors.New("ErrRecordNotFound") + ErrStructTransform = errors.New("ErrStructTransform") + ErrInitialPassword = errors.New("ErrInitialPassword") + ErrNotSupportType = errors.New("ErrNotSupportType") + ErrInvalidParams = errors.New("ErrInvalidParams") + + ErrTokenParse = errors.New("ErrTokenParse") +) + +// api +var ( + ErrTypeInternalServer = "ErrInternalServer" + ErrTypeInvalidParams = "ErrInvalidParams" + ErrTypeNotLogin = "ErrNotLogin" + ErrTypePasswordExpired = "ErrPasswordExpired" + ErrNameIsExist = "ErrNameIsExist" + ErrDemoEnvironment = "ErrDemoEnvironment" + ErrCmdIllegal = "ErrCmdIllegal" + ErrXpackNotFound = "ErrXpackNotFound" + ErrXpackNotActive = "ErrXpackNotActive" + ErrXpackOutOfDate = "ErrXpackOutOfDate" +) + +// app +var ( + ErrPortInUsed = "ErrPortInUsed" + ErrAppLimit = "ErrAppLimit" + ErrFileCanNotRead = "ErrFileCanNotRead" + ErrNotInstall = "ErrNotInstall" + ErrPortInOtherApp = "ErrPortInOtherApp" + ErrDbUserNotValid = "ErrDbUserNotValid" + ErrUpdateBuWebsite = "ErrUpdateBuWebsite" + Err1PanelNetworkFailed = "Err1PanelNetworkFailed" + ErrCmdTimeout = "ErrCmdTimeout" + ErrFileParse = "ErrFileParse" + ErrInstallDirNotFound = "ErrInstallDirNotFound" + ErrContainerName = "ErrContainerName" + ErrAppNameExist = "ErrAppNameExist" + ErrFileNotFound = "ErrFileNotFound" + ErrFileParseApp = "ErrFileParseApp" + ErrAppParamKey = "ErrAppParamKey" +) + +// website +var ( + ErrDomainIsExist = "ErrDomainIsExist" + ErrAliasIsExist = "ErrAliasIsExist" + ErrGroupIsUsed = "ErrGroupIsUsed" + ErrUsernameIsExist = "ErrUsernameIsExist" + ErrUsernameIsNotExist = "ErrUsernameIsNotExist" + ErrBackupMatch = "ErrBackupMatch" + ErrBackupExist = "ErrBackupExist" + ErrDomainIsUsed = "ErrDomainIsUsed" +) + +// ssl +var ( + ErrSSLCannotDelete = "ErrSSLCannotDelete" + ErrAccountCannotDelete = "ErrAccountCannotDelete" + ErrSSLApply = "ErrSSLApply" + ErrEmailIsExist = "ErrEmailIsExist" + ErrEabKidOrEabHmacKeyCannotBlank = "ErrEabKidOrEabHmacKeyCannotBlank" +) + +// file +var ( + ErrPathNotFound = "ErrPathNotFound" + ErrMovePathFailed = "ErrMovePathFailed" + ErrLinkPathNotFound = "ErrLinkPathNotFound" + ErrFileIsExist = "ErrFileIsExist" + ErrFileUpload = "ErrFileUpload" + ErrFileDownloadDir = "ErrFileDownloadDir" + ErrCmdNotFound = "ErrCmdNotFound" + ErrFavoriteExist = "ErrFavoriteExist" +) + +// mysql +var ( + ErrUserIsExist = "ErrUserIsExist" + ErrDatabaseIsExist = "ErrDatabaseIsExist" + ErrExecTimeOut = "ErrExecTimeOut" + ErrRemoteExist = "ErrRemoteExist" + ErrLocalExist = "ErrLocalExist" +) + +// redis +var ( + ErrTypeOfRedis = "ErrTypeOfRedis" +) + +// container +var ( + ErrInUsed = "ErrInUsed" + ErrObjectInUsed = "ErrObjectInUsed" + ErrPortRules = "ErrPortRules" + ErrPgImagePull = "ErrPgImagePull" +) + +// runtime +var ( + ErrDirNotFound = "ErrDirNotFound" + ErrFileNotExist = "ErrFileNotExist" + ErrImageBuildErr = "ErrImageBuildErr" + ErrImageExist = "ErrImageExist" + ErrDelWithWebsite = "ErrDelWithWebsite" + ErrRuntimeStart = "ErrRuntimeStart" + ErrPackageJsonNotFound = "ErrPackageJsonNotFound" + ErrScriptsNotFound = "ErrScriptsNotFound" +) + +var ( + ErrBackupInUsed = "ErrBackupInUsed" + ErrOSSConn = "ErrOSSConn" + ErrEntrance = "ErrEntrance" +) + +var ( + ErrFirewall = "ErrFirewall" +) + +// cronjob +var ( + ErrBashExecute = "ErrBashExecute" +) + +var ( + ErrNotExistUser = "ErrNotExistUser" +) + +// license +var ( + ErrLicense = "ErrLicense" + ErrLicenseCheck = "ErrLicenseCheck" + ErrLicenseSave = "ErrLicenseSave" + ErrLicenseSync = "ErrLicenseSync" +) diff --git a/agent/constant/host_tool.go b/agent/constant/host_tool.go new file mode 100644 index 000000000..60f483b02 --- /dev/null +++ b/agent/constant/host_tool.go @@ -0,0 +1,8 @@ +package constant + +const ( + Supervisord = "supervisord" + Supervisor = "supervisor" + SupervisorConfigPath = "SupervisorConfigPath" + SupervisorServiceName = "SupervisorServiceName" +) diff --git a/agent/constant/nginx.go b/agent/constant/nginx.go new file mode 100644 index 000000000..93b46b868 --- /dev/null +++ b/agent/constant/nginx.go @@ -0,0 +1,15 @@ +package constant + +const ( + NginxScopeServer = "server" + NginxScopeHttp = "http" + NginxScopeOut = "out" + + NginxReload = "reload" + NginxCheck = "check" + NginxRestart = "restart" + + ConfigNew = "add" + ConfigUpdate = "update" + ConfigDel = "delete" +) diff --git a/agent/constant/runtime.go b/agent/constant/runtime.go new file mode 100644 index 000000000..55a524983 --- /dev/null +++ b/agent/constant/runtime.go @@ -0,0 +1,35 @@ +package constant + +const ( + ResourceLocal = "local" + ResourceAppstore = "appstore" + + RuntimeNormal = "normal" + RuntimeError = "error" + RuntimeBuildIng = "building" + RuntimeStarting = "starting" + RuntimeRunning = "running" + RuntimeReCreating = "recreating" + RuntimeStopped = "stopped" + RuntimeUnhealthy = "unhealthy" + RuntimeCreating = "creating" + + RuntimePHP = "php" + RuntimeNode = "node" + RuntimeJava = "java" + RuntimeGo = "go" + + RuntimeProxyUnix = "unix" + RuntimeProxyTcp = "tcp" + + RuntimeUp = "up" + RuntimeDown = "down" + RuntimeRestart = "restart" + + RuntimeInstall = "install" + RuntimeUninstall = "uninstall" + RuntimeUpdate = "update" + + RuntimeNpm = "npm" + RuntimeYarn = "yarn" +) diff --git a/agent/constant/session.go b/agent/constant/session.go new file mode 100644 index 000000000..94fdf462d --- /dev/null +++ b/agent/constant/session.go @@ -0,0 +1,13 @@ +package constant + +const ( + AuthMethodSession = "session" + SessionName = "psession" + + AuthMethodJWT = "jwt" + JWTHeaderName = "PanelAuthorization" + JWTBufferTime = 3600 + JWTIssuer = "1Panel" + + PasswordExpiredName = "expired" +) diff --git a/agent/constant/status.go b/agent/constant/status.go new file mode 100644 index 000000000..161b50e00 --- /dev/null +++ b/agent/constant/status.go @@ -0,0 +1,16 @@ +package constant + +const ( + StatusRunning = "Running" + StatusDone = "Done" + StatusWaiting = "Waiting" + StatusSuccess = "Success" + StatusFailed = "Failed" + StatusUploading = "Uploading" + StatusEnable = "Enable" + StatusDisable = "Disable" + StatusNone = "None" + + OrderDesc = "descending" + OrderAsc = "ascending" +) diff --git a/agent/constant/website.go b/agent/constant/website.go new file mode 100644 index 000000000..a236fc6a8 --- /dev/null +++ b/agent/constant/website.go @@ -0,0 +1,51 @@ +package constant + +const ( + WebRunning = "Running" + WebStopped = "Stopped" + + ProtocolHTTP = "HTTP" + ProtocolHTTPS = "HTTPS" + + NewApp = "new" + InstalledApp = "installed" + + Deployment = "deployment" + Static = "static" + Proxy = "proxy" + Runtime = "runtime" + + SSLExisted = "existed" + SSLAuto = "auto" + SSLManual = "manual" + + DNSAccount = "dnsAccount" + DnsManual = "dnsManual" + Http = "http" + Manual = "manual" + SelfSigned = "selfSigned" + + StartWeb = "start" + StopWeb = "stop" + + HTTPSOnly = "HTTPSOnly" + HTTPAlso = "HTTPAlso" + HTTPToHTTPS = "HTTPToHTTPS" + + GetLog = "get" + DisableLog = "disable" + EnableLog = "enable" + DeleteLog = "delete" + + AccessLog = "access.log" + ErrorLog = "error.log" + + ConfigPHP = "php" + ConfigFPM = "fpm" + + SSLInit = "init" + SSLError = "error" + SSLReady = "ready" + SSLApply = "applying" + SSLApplyError = "applyError" +) diff --git a/agent/cron/cron.go b/agent/cron/cron.go new file mode 100644 index 000000000..e96719301 --- /dev/null +++ b/agent/cron/cron.go @@ -0,0 +1,102 @@ +package cron + +import ( + "fmt" + mathRand "math/rand" + "time" + + "github.com/1Panel-dev/1Panel/agent/app/model" + "github.com/1Panel-dev/1Panel/agent/app/repo" + "github.com/1Panel-dev/1Panel/agent/app/service" + "github.com/1Panel-dev/1Panel/agent/constant" + "github.com/1Panel-dev/1Panel/agent/cron/job" + "github.com/1Panel-dev/1Panel/agent/global" + "github.com/1Panel-dev/1Panel/agent/utils/common" + "github.com/1Panel-dev/1Panel/agent/utils/ntp" + "github.com/robfig/cron/v3" +) + +func Run() { + nyc, _ := time.LoadLocation(common.LoadTimeZone()) + global.Cron = cron.New(cron.WithLocation(nyc), cron.WithChain(cron.Recover(cron.DefaultLogger)), cron.WithChain(cron.DelayIfStillRunning(cron.DefaultLogger))) + + var ( + interval model.Setting + status model.Setting + ) + syncBeforeStart() + if err := global.DB.Where("key = ?", "MonitorStatus").Find(&status).Error; err != nil { + global.LOG.Errorf("load monitor status from db failed, err: %v", err) + } + if status.Value == "enable" { + if err := global.DB.Where("key = ?", "MonitorInterval").Find(&interval).Error; err != nil { + global.LOG.Errorf("load monitor interval from db failed, err: %v", err) + } + if err := service.StartMonitor(false, interval.Value); err != nil { + global.LOG.Errorf("can not add monitor corn job: %s", err.Error()) + } + } + + if _, err := global.Cron.AddJob("@daily", job.NewWebsiteJob()); err != nil { + global.LOG.Errorf("can not add website corn job: %s", err.Error()) + } + if _, err := global.Cron.AddJob("@daily", job.NewSSLJob()); err != nil { + global.LOG.Errorf("can not add ssl corn job: %s", err.Error()) + } + if _, err := global.Cron.AddJob(fmt.Sprintf("%v %v * * *", mathRand.Intn(60), mathRand.Intn(3)), job.NewAppStoreJob()); err != nil { + global.LOG.Errorf("can not add appstore corn job: %s", err.Error()) + } + if _, err := global.Cron.AddJob("@daily", job.NewCacheJob()); err != nil { + global.LOG.Errorf("can not add cache corn job: %s", err.Error()) + } + + var backup model.BackupAccount + _ = global.DB.Where("type = ?", "OneDrive").Find(&backup).Error + if backup.ID != 0 { + service.StartRefreshOneDriveToken() + } + global.Cron.Start() + + var cronJobs []model.Cronjob + if err := global.DB.Where("status = ?", constant.StatusEnable).Find(&cronJobs).Error; err != nil { + global.LOG.Errorf("start my cronjob failed, err: %v", err) + } + if err := global.DB.Model(&model.JobRecords{}). + Where("status = ?", constant.StatusRunning). + Updates(map[string]interface{}{ + "status": constant.StatusFailed, + "message": "Task Cancel", + "records": "errHandle", + }).Error; err != nil { + global.LOG.Errorf("start my cronjob failed, err: %v", err) + } + for i := 0; i < len(cronJobs); i++ { + entryIDs, err := service.NewICronjobService().StartJob(&cronJobs[i], false) + if err != nil { + global.LOG.Errorf("start %s job %s failed, err: %v", cronJobs[i].Type, cronJobs[i].Name, err) + } + if err := repo.NewICronjobRepo().Update(cronJobs[i].ID, map[string]interface{}{"entry_ids": entryIDs}); err != nil { + global.LOG.Errorf("update cronjob %s %s failed, err: %v", cronJobs[i].Type, cronJobs[i].Name, err) + } + } +} + +func syncBeforeStart() { + var ntpSite model.Setting + if err := global.DB.Where("key = ?", "NtpSite").Find(&ntpSite).Error; err != nil { + global.LOG.Errorf("load ntp serve from db failed, err: %v", err) + } + if len(ntpSite.Value) == 0 { + ntpSite.Value = "pool.ntp.org" + } + ntime, err := ntp.GetRemoteTime(ntpSite.Value) + if err != nil { + global.LOG.Errorf("load remote time with [%s] failed, err: %v", ntpSite.Value, err) + return + } + ts := ntime.Format(constant.DateTimeLayout) + if err := ntp.UpdateSystemTime(ts); err != nil { + global.LOG.Errorf("failed to synchronize system time with [%s], err: %v", ntpSite.Value, err) + } + global.LOG.Debugf("synchronize system time with [%s] successful!", ntpSite.Value) +} diff --git a/agent/cron/job/app.go b/agent/cron/job/app.go new file mode 100644 index 000000000..868c6f991 --- /dev/null +++ b/agent/cron/job/app.go @@ -0,0 +1,20 @@ +package job + +import ( + "github.com/1Panel-dev/1Panel/agent/app/service" + "github.com/1Panel-dev/1Panel/agent/global" +) + +type app struct{} + +func NewAppStoreJob() *app { + return &app{} +} + +func (a *app) Run() { + global.LOG.Info("AppStore scheduled task in progress ...") + if err := service.NewIAppService().SyncAppListFromRemote(); err != nil { + global.LOG.Errorf("AppStore sync failed %s", err.Error()) + } + global.LOG.Info("AppStore scheduled task has completed") +} diff --git a/agent/cron/job/cache.go b/agent/cron/job/cache.go new file mode 100644 index 000000000..371aac4bf --- /dev/null +++ b/agent/cron/job/cache.go @@ -0,0 +1,27 @@ +package job + +import ( + "time" + + "github.com/1Panel-dev/1Panel/agent/global" +) + +type Cache struct{} + +func NewCacheJob() *Cache { + return &Cache{} +} + +func (c *Cache) Run() { + global.LOG.Info("run cache gc start ...") + ticker := time.NewTicker(5 * time.Minute) + defer ticker.Stop() + for range ticker.C { + again: + err := global.CacheDb.RunValueLogGC(0.7) + if err == nil { + goto again + } + } + global.LOG.Info("run cache gc end ...") +} diff --git a/agent/cron/job/ssl.go b/agent/cron/job/ssl.go new file mode 100644 index 000000000..d090a025b --- /dev/null +++ b/agent/cron/job/ssl.go @@ -0,0 +1,77 @@ +package job + +import ( + "path" + "strings" + "time" + + "github.com/1Panel-dev/1Panel/agent/app/dto/request" + "github.com/1Panel-dev/1Panel/agent/app/repo" + "github.com/1Panel-dev/1Panel/agent/app/service" + "github.com/1Panel-dev/1Panel/agent/constant" + "github.com/1Panel-dev/1Panel/agent/global" + "github.com/1Panel-dev/1Panel/agent/utils/common" + "github.com/1Panel-dev/1Panel/agent/utils/files" +) + +type ssl struct { +} + +func NewSSLJob() *ssl { + return &ssl{} +} + +func (ssl *ssl) Run() { + systemSSLEnable, sslID := service.GetSystemSSL() + sslRepo := repo.NewISSLRepo() + sslService := service.NewIWebsiteSSLService() + sslList, _ := sslRepo.List() + nyc, _ := time.LoadLocation(common.LoadTimeZone()) + global.LOG.Info("The scheduled certificate update task is currently in progress ...") + now := time.Now().Add(10 * time.Second) + for _, s := range sslList { + if !s.AutoRenew || s.Provider == "manual" || s.Provider == "dnsManual" || s.Status == "applying" { + continue + } + expireDate := s.ExpireDate.In(nyc) + sub := expireDate.Sub(now) + if sub.Hours() < 720 { + global.LOG.Infof("Update the SSL certificate for the [%s] domain", s.PrimaryDomain) + if s.Provider == constant.SelfSigned { + caService := service.NewIWebsiteCAService() + if _, err := caService.ObtainSSL(request.WebsiteCAObtain{ + ID: s.CaID, + SSLID: s.ID, + Renew: true, + Unit: "year", + Time: 10, + }); err != nil { + global.LOG.Errorf("Failed to update the SSL certificate for the [%s] domain , err:%s", s.PrimaryDomain, err.Error()) + continue + } + } else { + if err := sslService.ObtainSSL(request.WebsiteSSLApply{ + ID: s.ID, + }); err != nil { + global.LOG.Errorf("Failed to update the SSL certificate for the [%s] domain , err:%s", s.PrimaryDomain, err.Error()) + continue + } + } + if systemSSLEnable && sslID == s.ID { + websiteSSL, _ := sslRepo.GetFirst(repo.NewCommonRepo().WithByID(s.ID)) + fileOp := files.NewFileOp() + secretDir := path.Join(global.CONF.System.BaseDir, "1panel/secret") + if err := fileOp.WriteFile(path.Join(secretDir, "server.crt"), strings.NewReader(websiteSSL.Pem), 0600); err != nil { + global.LOG.Errorf("Failed to update the SSL certificate File for 1Panel System domain [%s] , err:%s", s.PrimaryDomain, err.Error()) + continue + } + if err := fileOp.WriteFile(path.Join(secretDir, "server.key"), strings.NewReader(websiteSSL.PrivateKey), 0600); err != nil { + global.LOG.Errorf("Failed to update the SSL certificate for 1Panel System domain [%s] , err:%s", s.PrimaryDomain, err.Error()) + continue + } + } + global.LOG.Infof("The SSL certificate for the [%s] domain has been successfully updated", s.PrimaryDomain) + } + } + global.LOG.Info("The scheduled certificate update task has completed") +} diff --git a/agent/cron/job/website.go b/agent/cron/job/website.go new file mode 100644 index 000000000..703d329b0 --- /dev/null +++ b/agent/cron/job/website.go @@ -0,0 +1,63 @@ +package job + +import ( + "sync" + "time" + + "github.com/1Panel-dev/1Panel/agent/app/dto/request" + "github.com/1Panel-dev/1Panel/agent/app/model" + "github.com/1Panel-dev/1Panel/agent/app/repo" + "github.com/1Panel-dev/1Panel/agent/app/service" + "github.com/1Panel-dev/1Panel/agent/constant" + "github.com/1Panel-dev/1Panel/agent/global" + "github.com/1Panel-dev/1Panel/agent/utils/common" +) + +type website struct{} + +func NewWebsiteJob() *website { + return &website{} +} + +func (w *website) Run() { + nyc, _ := time.LoadLocation(common.LoadTimeZone()) + websites, _ := repo.NewIWebsiteRepo().List() + global.LOG.Info("Website scheduled task in progress ...") + now := time.Now().Add(10 * time.Minute) + if len(websites) > 0 { + neverExpireDate, _ := time.Parse(constant.DateLayout, constant.DefaultDate) + var wg sync.WaitGroup + for _, site := range websites { + if site.Status != constant.WebRunning || neverExpireDate.Equal(site.ExpireDate) { + continue + } + expireDate, err := time.ParseInLocation(constant.DateLayout, site.ExpireDate.Format(constant.DateLayout), nyc) + if err != nil { + global.LOG.Errorf("time parse err %v", err) + continue + } + if expireDate.Before(now) { + wg.Add(1) + go func(ws model.Website) { + stopWebsite(ws.ID, ws.PrimaryDomain, &wg) + }(site) + } + } + wg.Wait() + } + global.LOG.Info("Website scheduled task has completed") +} + +func stopWebsite(websiteId uint, websiteName string, wg *sync.WaitGroup) { + websiteService := service.NewIWebsiteService() + req := request.WebsiteOp{ + ID: websiteId, + Operate: constant.StopWeb, + } + if err := websiteService.OpWebsite(req); err != nil { + global.LOG.Errorf("Website [%s] seop failed err %v", websiteName, err) + } else { + global.LOG.Infof("Website [%s] stopped successfully", websiteName) + } + wg.Done() +} diff --git a/agent/global/global.go b/agent/global/global.go new file mode 100644 index 000000000..7a797db47 --- /dev/null +++ b/agent/global/global.go @@ -0,0 +1,30 @@ +package global + +import ( + "github.com/1Panel-dev/1Panel/agent/configs" + "github.com/1Panel-dev/1Panel/agent/init/cache/badger_db" + "github.com/dgraph-io/badger/v4" + "github.com/go-playground/validator/v10" + "github.com/nicksnyder/go-i18n/v2/i18n" + "github.com/robfig/cron/v3" + "github.com/sirupsen/logrus" + "github.com/spf13/viper" + "gorm.io/gorm" +) + +var ( + DB *gorm.DB + MonitorDB *gorm.DB + LOG *logrus.Logger + CONF configs.ServerConfig + VALID *validator.Validate + CACHE *badger_db.Cache + CacheDb *badger.DB + Viper *viper.Viper + + Cron *cron.Cron + MonitorCronID cron.EntryID + OneDriveCronID cron.EntryID + + I18n *i18n.Localizer +) diff --git a/agent/go.mod b/agent/go.mod new file mode 100644 index 000000000..ae1f5c447 --- /dev/null +++ b/agent/go.mod @@ -0,0 +1,266 @@ +module github.com/1Panel-dev/1Panel/agent + +go 1.22.4 + +require ( + github.com/aliyun/aliyun-oss-go-sdk v3.0.2+incompatible + github.com/aws/aws-sdk-go v1.55.0 + github.com/compose-spec/compose-go/v2 v2.1.4 + github.com/creack/pty v1.1.21 + github.com/dgraph-io/badger/v4 v4.2.0 + github.com/docker/compose/v2 v2.29.0 + github.com/docker/docker v27.1.0+incompatible + github.com/docker/go-connections v0.5.0 + github.com/fsnotify/fsnotify v1.7.0 + github.com/gin-contrib/gzip v1.0.1 + github.com/gin-gonic/gin v1.10.0 + github.com/glebarez/sqlite v1.11.0 + github.com/go-acme/lego/v4 v4.17.4 + github.com/go-gormigrate/gormigrate/v2 v2.1.2 + github.com/go-playground/validator/v10 v10.22.0 + github.com/go-redis/redis v6.15.9+incompatible + github.com/go-sql-driver/mysql v1.8.1 + github.com/goh-chunlin/go-onedrive v1.1.1 + github.com/golang-jwt/jwt/v4 v4.5.0 + github.com/google/uuid v1.6.0 + github.com/gorilla/websocket v1.5.3 + github.com/jackc/pgx/v5 v5.6.0 + github.com/jinzhu/copier v0.4.0 + github.com/jinzhu/gorm v1.9.16 + github.com/joho/godotenv v1.5.1 + github.com/klauspost/compress v1.17.9 + github.com/mholt/archiver/v4 v4.0.0-alpha.8 + github.com/minio/minio-go/v7 v7.0.74 + github.com/nicksnyder/go-i18n/v2 v2.4.0 + github.com/opencontainers/image-spec v1.1.0 + github.com/pkg/errors v0.9.1 + github.com/pkg/sftp v1.13.6 + github.com/qiniu/go-sdk/v7 v7.21.1 + github.com/robfig/cron/v3 v3.0.1 + github.com/shirou/gopsutil/v3 v3.24.5 + github.com/sirupsen/logrus v1.9.3 + github.com/spf13/afero v1.11.0 + github.com/spf13/viper v1.19.0 + github.com/studio-b12/gowebdav v0.9.0 + github.com/subosito/gotenv v1.6.0 + github.com/swaggo/swag v1.16.3 + github.com/tencentyun/cos-go-sdk-v5 v0.7.54 + golang.org/x/crypto v0.25.0 + golang.org/x/net v0.27.0 + golang.org/x/oauth2 v0.21.0 + golang.org/x/sys v0.22.0 + golang.org/x/text v0.16.0 + gopkg.in/ini.v1 v1.67.0 + gopkg.in/yaml.v3 v3.0.1 + gorm.io/gorm v1.25.11 +) + +require ( + filippo.io/edwards25519 v1.1.0 // indirect + github.com/AdaLogics/go-fuzz-headers v0.0.0-20230811130428-ced1acdcaa24 // indirect + github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 // indirect + github.com/BurntSushi/toml v1.4.0 // indirect + github.com/KyleBanks/depth v1.2.1 // indirect + github.com/Microsoft/go-winio v0.6.2 // indirect + github.com/alex-ant/gomath v0.0.0-20160516115720-89013a210a82 // indirect + github.com/aliyun/alibaba-cloud-sdk-go v1.62.712 // indirect + github.com/andybalholm/brotli v1.0.4 // indirect + github.com/beorn7/perks v1.0.1 // indirect + github.com/bodgit/plumbing v1.2.0 // indirect + github.com/bodgit/sevenzip v1.3.0 // indirect + github.com/bodgit/windows v1.0.0 // indirect + github.com/bytedance/sonic v1.11.6 // indirect + github.com/bytedance/sonic/loader v0.1.1 // indirect + github.com/cenkalti/backoff/v4 v4.3.0 // indirect + github.com/cespare/xxhash/v2 v2.2.0 // indirect + github.com/clbanning/mxj v1.8.4 // indirect + github.com/cloudflare/cloudflare-go v0.97.0 // indirect + github.com/cloudwego/base64x v0.1.4 // indirect + github.com/cloudwego/iasm v0.2.0 // indirect + github.com/connesc/cipherio v0.2.1 // indirect + github.com/containerd/console v1.0.4 // indirect + github.com/containerd/containerd v1.7.19 // indirect + github.com/containerd/containerd/api v1.7.19 // indirect + github.com/containerd/continuity v0.4.3 // indirect + github.com/containerd/errdefs v0.1.0 // indirect + github.com/containerd/log v0.1.0 // indirect + github.com/containerd/platforms v0.2.1 // indirect + github.com/containerd/ttrpc v1.2.5 // indirect + github.com/containerd/typeurl/v2 v2.1.1 // indirect + github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect + github.com/dgraph-io/ristretto v0.1.1 // indirect + github.com/distribution/reference v0.6.0 // indirect + github.com/docker/buildx v0.16.0 // indirect + github.com/docker/cli v27.0.3+incompatible // indirect + github.com/docker/distribution v2.8.3+incompatible // indirect + github.com/docker/docker-credential-helpers v0.8.2 // indirect + github.com/docker/go v1.5.1-1.0.20160303222718-d30aec9fd63c // indirect + github.com/docker/go-metrics v0.0.1 // indirect + github.com/docker/go-units v0.5.0 // indirect + github.com/dsnet/compress v0.0.1 // indirect + github.com/dustin/go-humanize v1.0.1 // indirect + github.com/felixge/httpsnoop v1.0.4 // indirect + github.com/fvbommel/sortorder v1.0.2 // indirect + github.com/gabriel-vasile/mimetype v1.4.3 // indirect + github.com/gin-contrib/sse v0.1.0 // indirect + github.com/glebarez/go-sqlite v1.21.2 // indirect + github.com/go-ini/ini v1.67.0 // indirect + github.com/go-jose/go-jose/v4 v4.0.2 // indirect + github.com/go-logr/logr v1.4.1 // indirect + github.com/go-logr/stdr v1.2.2 // indirect + github.com/go-ole/go-ole v1.2.6 // indirect + github.com/go-openapi/jsonpointer v0.19.6 // indirect + github.com/go-openapi/jsonreference v0.20.2 // indirect + github.com/go-openapi/spec v0.20.4 // indirect + github.com/go-openapi/swag v0.22.3 // indirect + github.com/go-playground/locales v0.14.1 // indirect + github.com/go-playground/universal-translator v0.18.1 // indirect + github.com/go-viper/mapstructure/v2 v2.0.0 // indirect + github.com/goccy/go-json v0.10.3 // indirect + github.com/gofrs/flock v0.12.0 // indirect + github.com/gogo/googleapis v1.4.1 // indirect + github.com/gogo/protobuf v1.3.2 // indirect + github.com/golang/glog v1.2.0 // indirect + github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect + github.com/golang/protobuf v1.5.4 // indirect + github.com/golang/snappy v0.0.4 // indirect + github.com/google/flatbuffers v1.12.1 // indirect + github.com/google/go-cmp v0.6.0 // indirect + github.com/google/go-querystring v1.1.0 // indirect + github.com/google/gofuzz v1.2.0 // indirect + github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 // indirect + github.com/gorilla/mux v1.8.1 // indirect + github.com/grpc-ecosystem/grpc-gateway/v2 v2.16.0 // indirect + github.com/h2non/filetype v1.1.1 // indirect + github.com/hashicorp/errwrap v1.1.0 // indirect + github.com/hashicorp/go-cleanhttp v0.5.2 // indirect + github.com/hashicorp/go-multierror v1.1.1 // indirect + github.com/hashicorp/go-retryablehttp v0.7.7 // indirect + github.com/hashicorp/go-version v1.7.0 // indirect + github.com/hashicorp/hcl v1.0.0 // indirect + github.com/imdario/mergo v0.3.16 // indirect + github.com/in-toto/in-toto-golang v0.5.0 // indirect + github.com/inconshreveable/mousetrap v1.1.0 // indirect + github.com/jackc/pgpassfile v1.0.0 // indirect + github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a // indirect + github.com/jackc/puddle/v2 v2.2.1 // indirect + github.com/jinzhu/inflection v1.0.0 // indirect + github.com/jinzhu/now v1.1.5 // indirect + github.com/jmespath/go-jmespath v0.4.0 // indirect + github.com/josharian/intern v1.0.0 // indirect + github.com/json-iterator/go v1.1.12 // indirect + github.com/klauspost/cpuid/v2 v2.2.8 // indirect + github.com/klauspost/pgzip v1.2.5 // indirect + github.com/kr/fs v0.1.0 // indirect + github.com/leodido/go-urn v1.4.0 // indirect + github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 // indirect + github.com/magiconair/properties v1.8.7 // indirect + github.com/mailru/easyjson v0.7.7 // indirect + github.com/matishsiao/goInfo v0.0.0-20210923090445-da2e3fa8d45f // indirect + github.com/mattn/go-isatty v0.0.20 // indirect + github.com/mattn/go-shellwords v1.0.12 // indirect + github.com/matttproud/golang_protobuf_extensions v1.0.4 // indirect + github.com/miekg/dns v1.1.59 // indirect + github.com/miekg/pkcs11 v1.1.1 // indirect + github.com/minio/md5-simd v1.1.2 // indirect + github.com/mitchellh/hashstructure/v2 v2.0.2 // indirect + github.com/mitchellh/mapstructure v1.5.0 // indirect + github.com/moby/buildkit v0.15.0 // indirect + github.com/moby/docker-image-spec v1.3.1 // indirect + github.com/moby/locker v1.0.1 // indirect + github.com/moby/patternmatcher v0.6.0 // indirect + github.com/moby/sys/sequential v0.5.0 // indirect + github.com/moby/sys/signal v0.7.0 // indirect + github.com/moby/sys/user v0.1.0 // indirect + github.com/moby/term v0.5.0 // indirect + github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect + github.com/modern-go/reflect2 v1.0.2 // indirect + github.com/morikuni/aec v1.0.0 // indirect + github.com/mozillazg/go-httpheader v0.2.1 // indirect + github.com/namedotcom/go v0.0.0-20180403034216-08470befbe04 // indirect + github.com/nrdcg/dnspod-go v0.4.0 // indirect + github.com/nrdcg/namesilo v0.2.1 // indirect + github.com/nwaples/rardecode/v2 v2.0.0-beta.2 // indirect + github.com/opencontainers/go-digest v1.0.0 // indirect + github.com/opentracing/opentracing-go v1.2.1-0.20220228012449-10b1cf09e00b // indirect + github.com/pelletier/go-toml v1.9.5 // indirect + github.com/pelletier/go-toml/v2 v2.2.2 // indirect + github.com/pierrec/lz4/v4 v4.1.15 // indirect + github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect + github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c // indirect + github.com/prometheus/client_golang v1.17.0 // indirect + github.com/prometheus/client_model v0.5.0 // indirect + github.com/prometheus/common v0.44.0 // indirect + github.com/prometheus/procfs v0.15.1 // indirect + github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect + github.com/rs/xid v1.5.0 // indirect + github.com/sagikazarmark/locafero v0.4.0 // indirect + github.com/sagikazarmark/slog-shim v0.1.0 // indirect + github.com/secure-systems-lab/go-securesystemslib v0.4.0 // indirect + github.com/shibumi/go-pathspec v1.3.0 // indirect + github.com/shoenig/go-m1cpu v0.1.6 // indirect + github.com/sourcegraph/conc v0.3.0 // indirect + github.com/spf13/cast v1.6.0 // indirect + github.com/spf13/cobra v1.8.1 // indirect + github.com/spf13/pflag v1.0.5 // indirect + github.com/stretchr/testify v1.9.0 // indirect + github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.0.898 // indirect + github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/dnspod v1.0.898 // indirect + github.com/therootcompany/xz v1.0.1 // indirect + github.com/theupdateframework/notary v0.7.0 // indirect + github.com/tklauser/go-sysconf v0.3.12 // indirect + github.com/tklauser/numcpus v0.6.1 // indirect + github.com/tonistiigi/fsutil v0.0.0-20240424095704-91a3fc46842c // indirect + github.com/tonistiigi/go-csvvalue v0.0.0-20240710180619-ddb21b71c0b4 // indirect + github.com/tonistiigi/units v0.0.0-20180711220420-6950e57a87ea // indirect + github.com/tonistiigi/vt100 v0.0.0-20240514184818-90bafcd6abab // indirect + github.com/twitchyliquid64/golang-asm v0.15.1 // indirect + github.com/ugorji/go/codec v1.2.12 // indirect + github.com/ulikunitz/xz v0.5.10 // indirect + github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb // indirect + github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect + github.com/xeipuuv/gojsonschema v1.2.0 // indirect + github.com/yusufpapurcu/wmi v1.2.4 // indirect + go.opencensus.io v0.24.0 // indirect + go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.49.0 // indirect + go.opentelemetry.io/contrib/instrumentation/net/http/httptrace/otelhttptrace v0.46.1 // indirect + go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.49.0 // indirect + go.opentelemetry.io/otel v1.24.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlpmetric v0.42.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v0.42.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.21.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.21.0 // indirect + go.opentelemetry.io/otel/metric v1.24.0 // indirect + go.opentelemetry.io/otel/sdk v1.21.0 // indirect + go.opentelemetry.io/otel/sdk/metric v1.21.0 // indirect + go.opentelemetry.io/otel/trace v1.24.0 // indirect + go.opentelemetry.io/proto/otlp v1.0.0 // indirect + go.uber.org/atomic v1.9.0 // indirect + go.uber.org/multierr v1.9.0 // indirect + go4.org v0.0.0-20200411211856-f5505b9728dd // indirect + golang.org/x/arch v0.8.0 // indirect + golang.org/x/exp v0.0.0-20240112132812-db7319d0e0e3 // indirect + golang.org/x/mod v0.18.0 // indirect + golang.org/x/sync v0.7.0 // indirect + golang.org/x/term v0.22.0 // indirect + golang.org/x/time v0.5.0 // indirect + golang.org/x/tools v0.22.0 // indirect + google.golang.org/genproto v0.0.0-20240227224415-6ceb2ff114de // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20240311132316-a219d84964c2 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20240318140521-94a12d6c2237 // indirect + google.golang.org/grpc v1.63.1 // indirect + google.golang.org/protobuf v1.34.1 // indirect + gopkg.in/inf.v0 v0.9.1 // indirect + gopkg.in/yaml.v2 v2.4.0 // indirect + k8s.io/apimachinery v0.29.2 // indirect + k8s.io/client-go v0.29.2 // indirect + k8s.io/klog/v2 v2.110.1 // indirect + k8s.io/utils v0.0.0-20230726121419-3b25d923346b // indirect + modernc.org/libc v1.22.5 // indirect + modernc.org/mathutil v1.5.0 // indirect + modernc.org/memory v1.5.0 // indirect + modernc.org/sqlite v1.23.1 // indirect + sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd // indirect + sigs.k8s.io/structured-merge-diff/v4 v4.4.1 // indirect + sigs.k8s.io/yaml v1.3.0 // indirect +) diff --git a/agent/go.sum b/agent/go.sum new file mode 100644 index 000000000..e248f6aca --- /dev/null +++ b/agent/go.sum @@ -0,0 +1,1237 @@ +cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU= +cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU= +cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY= +cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc= +cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0= +cloud.google.com/go v0.50.0/go.mod h1:r9sluTvynVuxRIOHXQEHMFffphuXHOMZMycpNR5e6To= +cloud.google.com/go v0.52.0/go.mod h1:pXajvRH/6o3+F9jDHZWQ5PbGhn+o8w9qiu/CffaVdO4= +cloud.google.com/go v0.53.0/go.mod h1:fp/UouUEsRkN6ryDKNW/Upv/JBKnv6WDthjR6+vze6M= +cloud.google.com/go v0.54.0/go.mod h1:1rq2OEkV3YMf6n/9ZvGWI3GWw0VoqH/1x2nd8Is/bPc= +cloud.google.com/go v0.56.0/go.mod h1:jr7tqZxxKOVYizybht9+26Z/gUq7tiRzu+ACVAMbKVk= +cloud.google.com/go v0.57.0/go.mod h1:oXiQ6Rzq3RAkkY7N6t3TcE6jE+CIBBbA36lwQ1JyzZs= +cloud.google.com/go v0.62.0/go.mod h1:jmCYTdRCQuc1PHIIJ/maLInMho30T/Y0M4hTdTShOYc= +cloud.google.com/go v0.65.0/go.mod h1:O5N8zS7uWy9vkA9vayVHs65eM1ubvY4h553ofrNHObY= +cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o= +cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE= +cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc= +cloud.google.com/go/bigquery v1.5.0/go.mod h1:snEHRnqQbz117VIFhE8bmtwIDY80NLUZUMb4Nv6dBIg= +cloud.google.com/go/bigquery v1.7.0/go.mod h1://okPTzCYNXSlb24MZs83e2Do+h+VXtc4gLoIoXIAPc= +cloud.google.com/go/bigquery v1.8.0/go.mod h1:J5hqkt3O0uAFnINi6JXValWIb1v0goeZM77hZzJN/fQ= +cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE= +cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk= +cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I= +cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw= +cloud.google.com/go/pubsub v1.2.0/go.mod h1:jhfEVHT8odbXTkndysNHCcx0awwzvfOlguIAii9o8iA= +cloud.google.com/go/pubsub v1.3.1/go.mod h1:i+ucay31+CNRpDW4Lu78I4xXG+O1r/MAHgjpRVR+TSU= +cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw= +cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos= +cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk= +cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs= +cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0= +dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= +filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA= +filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4= +github.com/AdaLogics/go-fuzz-headers v0.0.0-20230811130428-ced1acdcaa24 h1:bvDV9vkmnHYOMsOr4WLk+Vo07yKIzd94sVoIqshQ4bU= +github.com/AdaLogics/go-fuzz-headers v0.0.0-20230811130428-ced1acdcaa24/go.mod h1:8o94RPi1/7XTJvwPpRSzSUedZrtlirdB3r9Z20bi2f8= +github.com/AdamKorcz/go-118-fuzz-build v0.0.0-20230306123547-8075edf89bb0 h1:59MxjQVfjXsBpLy+dbd2/ELV5ofnUkUZBvWSC85sheA= +github.com/AdamKorcz/go-118-fuzz-build v0.0.0-20230306123547-8075edf89bb0/go.mod h1:OahwfttHWG6eJ0clwcfBAHoDI6X/LV/15hx/wlMZSrU= +github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 h1:UQHMgLO+TxOElx5B5HZ4hJQsoJ/PvUvKRhJHDQXO8P8= +github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E= +github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +github.com/BurntSushi/toml v1.3.2/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ= +github.com/BurntSushi/toml v1.4.0 h1:kuoIxZQy2WRRk1pttg9asf+WVv6tWQuBNVmK8+nqPr0= +github.com/BurntSushi/toml v1.4.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho= +github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= +github.com/HdrHistogram/hdrhistogram-go v1.1.2/go.mod h1:yDgFjdqOqDEKOvasDdhWNXYg9BVp4O+o5f6V/ehm6Oo= +github.com/KyleBanks/depth v1.2.1 h1:5h8fQADFrWtarTdtDudMmGsC7GPbOAu6RVB3ffsVFHc= +github.com/KyleBanks/depth v1.2.1/go.mod h1:jzSb9d0L43HxTQfT+oSA1EEp2q+ne2uh6XgeJcm8brE= +github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY= +github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU= +github.com/Microsoft/hcsshim v0.11.7 h1:vl/nj3Bar/CvJSYo7gIQPyRWc9f3c6IeSNavBTSZNZQ= +github.com/Microsoft/hcsshim v0.11.7/go.mod h1:MV8xMfmECjl5HdO7U/3/hFVnkmSBjAjmA09d4bExKcU= +github.com/PuerkitoBio/goquery v1.5.1/go.mod h1:GsLWisAFVj4WgDibEWF4pvYnkVQBpKBKeU+7zCJoLcc= +github.com/PuerkitoBio/purell v1.1.1/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0= +github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE= +github.com/QcloudApi/qcloud_sign_golang v0.0.0-20141224014652-e4130a326409/go.mod h1:1pk82RBxDY/JZnPQrtqHlUFfCctgdorsd9M06fMynOM= +github.com/Shopify/logrus-bugsnag v0.0.0-20170309145241-6dbc35f2c30d/go.mod h1:HI8ITrYtUY+O+ZhtlqUnD8+KwNPOyugEhfP9fdUIaEQ= +github.com/Shopify/logrus-bugsnag v0.0.0-20171204204709-577dee27f20d h1:UrqY+r/OJnIp5u0s1SbQ8dVfLCZJsnvazdBP5hS4iRs= +github.com/Shopify/logrus-bugsnag v0.0.0-20171204204709-577dee27f20d/go.mod h1:HI8ITrYtUY+O+ZhtlqUnD8+KwNPOyugEhfP9fdUIaEQ= +github.com/ajstarks/svgo v0.0.0-20180226025133-644b8db467af/go.mod h1:K08gAheRH3/J6wwsYMMT4xOr94bZjxIelGM0+d/wbFw= +github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= +github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= +github.com/alex-ant/gomath v0.0.0-20160516115720-89013a210a82 h1:7dONQ3WNZ1zy960TmkxJPuwoolZwL7xKtpcM04MBnt4= +github.com/alex-ant/gomath v0.0.0-20160516115720-89013a210a82/go.mod h1:nLnM0KdK1CmygvjpDUO6m1TjSsiQtL61juhNsvV/JVI= +github.com/aliyun/alibaba-cloud-sdk-go v1.62.712 h1:lM7JnA9dEdDFH9XOgRNQMDTQnOjlLkDTNA7c0aWTQ30= +github.com/aliyun/alibaba-cloud-sdk-go v1.62.712/go.mod h1:SOSDHfe1kX91v3W5QiBsWSLqeLxImobbMX1mxrFHsVQ= +github.com/aliyun/aliyun-oss-go-sdk v3.0.2+incompatible h1:8psS8a+wKfiLt1iVDX79F7Y6wUM49Lcha2FMXt4UM8g= +github.com/aliyun/aliyun-oss-go-sdk v3.0.2+incompatible/go.mod h1:T/Aws4fEfogEE9v+HPhhw+CntffsBHJ8nXQCwKr0/g8= +github.com/anchore/go-struct-converter v0.0.0-20221118182256-c68fdcfa2092 h1:aM1rlcoLz8y5B2r4tTLMiVTrMtpfY0O8EScKJxaSaEc= +github.com/anchore/go-struct-converter v0.0.0-20221118182256-c68fdcfa2092/go.mod h1:rYqSE9HbjzpHTI74vwPvae4ZVYZd1lue2ta6xHPdblA= +github.com/andybalholm/brotli v1.0.4 h1:V7DdXeJtZscaqfNuAdSRuRFzuiKlHSC/Zh3zl9qY3JY= +github.com/andybalholm/brotli v1.0.4/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig= +github.com/andybalholm/cascadia v1.1.0/go.mod h1:GsXiBklL0woXo1j/WYWtSYYC4ouU9PqHO0sqidkEA4Y= +github.com/aws/aws-sdk-go v1.55.0 h1:hVALKPjXz33kP1R9nTyJpUK7qF59dO2mleQxUW9mCVE= +github.com/aws/aws-sdk-go v1.55.0/go.mod h1:eRwEWoyTWFMVYVQzKMNHWP5/RV4xIUGMQfXQHfHkpNU= +github.com/beorn7/perks v0.0.0-20150223135152-b965b613227f/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= +github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= +github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= +github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= +github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= +github.com/bitly/go-hostpool v0.1.0/go.mod h1:4gOCgp6+NZnVqlKyZ/iBZFTAJKembaVENUpMkpg42fw= +github.com/bitly/go-simplejson v0.5.0/go.mod h1:cXHtHw4XUPsvGaxgjIAn8PhEWG9NfngEKAMDJEczWVA= +github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869/go.mod h1:Ekp36dRnpXw/yCqJaO+ZrUyxD+3VXMFFr56k5XYrpB4= +github.com/bodgit/plumbing v1.2.0 h1:gg4haxoKphLjml+tgnecR4yLBV5zo4HAZGCtAh3xCzM= +github.com/bodgit/plumbing v1.2.0/go.mod h1:b9TeRi7Hvc6Y05rjm8VML3+47n4XTZPtQ/5ghqic2n8= +github.com/bodgit/sevenzip v1.3.0 h1:1ljgELgtHqvgIp8W8kgeEGHIWP4ch3xGI8uOBZgLVKY= +github.com/bodgit/sevenzip v1.3.0/go.mod h1:omwNcgZTEooWM8gA/IJ2Nk/+ZQ94+GsytRzOJJ8FBlM= +github.com/bodgit/windows v1.0.0 h1:rLQ/XjsleZvx4fR1tB/UxQrK+SJ2OFHzfPjLWWOhDIA= +github.com/bodgit/windows v1.0.0/go.mod h1:a6JLwrB4KrTR5hBpp8FI9/9W9jJfeQ2h4XDXU74ZCdM= +github.com/bugsnag/bugsnag-go v1.0.5-0.20150529004307-13fd6b8acda0 h1:s7+5BfS4WFJoVF9pnB8kBk03S7pZXRdKamnV0FOl5Sc= +github.com/bugsnag/bugsnag-go v1.0.5-0.20150529004307-13fd6b8acda0/go.mod h1:2oa8nejYd4cQ/b0hMIopN0lCRxU0bueqREvZLWFrtK8= +github.com/bugsnag/osext v0.0.0-20130617224835-0dd3f918b21b h1:otBG+dV+YK+Soembjv71DPz3uX/V/6MMlSyD9JBQ6kQ= +github.com/bugsnag/osext v0.0.0-20130617224835-0dd3f918b21b/go.mod h1:obH5gd0BsqsP2LwDJ9aOkm/6J86V6lyAXCoQWGw3K50= +github.com/bugsnag/panicwrap v0.0.0-20151223152923-e2c28503fcd0 h1:nvj0OLI3YqYXer/kZD8Ri1aaunCxIEsOst1BVJswV0o= +github.com/bugsnag/panicwrap v0.0.0-20151223152923-e2c28503fcd0/go.mod h1:D/8v3kj0zr8ZAKg1AQ6crr+5VwKN5eIywRkfhyM/+dE= +github.com/bytedance/sonic v1.11.6 h1:oUp34TzMlL+OY1OUWxHqsdkgC/Zfc85zGqw9siXjrc0= +github.com/bytedance/sonic v1.11.6/go.mod h1:LysEHSvpvDySVdC2f87zGWf6CIKJcAvqab1ZaiQtds4= +github.com/bytedance/sonic/loader v0.1.1 h1:c+e5Pt1k/cy5wMveRDyk2X4B9hF4g7an8N3zCYjJFNM= +github.com/bytedance/sonic/loader v0.1.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU= +github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8= +github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= +github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= +github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44= +github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= +github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= +github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= +github.com/clbanning/mxj v1.8.4 h1:HuhwZtbyvyOw+3Z1AowPkU87JkJUSv751ELWaiTpj8I= +github.com/clbanning/mxj v1.8.4/go.mod h1:BVjHeAH+rl9rs6f+QIpeRl0tfu10SXn1pUSa5PVGJng= +github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= +github.com/cloudflare/cfssl v0.0.0-20180223231731-4e2dcbde5004 h1:lkAMpLVBDaj17e85keuznYcH5rqI438v41pKcBl4ZxQ= +github.com/cloudflare/cfssl v0.0.0-20180223231731-4e2dcbde5004/go.mod h1:yMWuSON2oQp+43nFtAV/uvKQIFpSPerB57DCt9t8sSA= +github.com/cloudflare/cloudflare-go v0.97.0 h1:feZRGiRF1EbljnNIYdt8014FnOLtC3CCvgkLXu915ks= +github.com/cloudflare/cloudflare-go v0.97.0/go.mod h1:JXRwuTfHpe5xFg8xytc2w0XC6LcrFsBVMS4WlVaiGg8= +github.com/cloudwego/base64x v0.1.4 h1:jwCgWpFanWmN8xoIUHa2rtzmkd5J2plF/dnLS6Xd/0Y= +github.com/cloudwego/base64x v0.1.4/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w= +github.com/cloudwego/iasm v0.2.0 h1:1KNIy1I1H9hNNFEEH3DVnI4UujN+1zjpuk6gwHLTssg= +github.com/cloudwego/iasm v0.2.0/go.mod h1:8rXZaNYT2n95jn+zTI1sDr+IgcD2GVs0nlbbQPiEFhY= +github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= +github.com/codahale/rfc6979 v0.0.0-20141003034818-6a90f24967eb h1:EDmT6Q9Zs+SbUoc7Ik9EfrFqcylYqgPZ9ANSbTAntnE= +github.com/codahale/rfc6979 v0.0.0-20141003034818-6a90f24967eb/go.mod h1:ZjrT6AXHbDs86ZSdt/osfBi5qfexBrKUdONk989Wnk4= +github.com/compose-spec/compose-go/v2 v2.1.4 h1:+1UKMvbBJo22Bpulgb9KAeZwRT99hANf3tDQVeG6ZJo= +github.com/compose-spec/compose-go/v2 v2.1.4/go.mod h1:lFN0DrMxIncJGYAXTfWuajfwj5haBJqrBkarHcnjJKc= +github.com/connesc/cipherio v0.2.1 h1:FGtpTPMbKNNWByNrr9aEBtaJtXjqOzkIXNYJp6OEycw= +github.com/connesc/cipherio v0.2.1/go.mod h1:ukY0MWJDFnJEbXMQtOcn2VmTpRfzcTz4OoVrWGGJZcA= +github.com/containerd/cgroups v1.1.0 h1:v8rEWFl6EoqHB+swVNjVoCJE8o3jX7e8nqBGPLaDFBM= +github.com/containerd/cgroups v1.1.0/go.mod h1:6ppBcbh/NOOUU+dMKrykgaBnK9lCIBxHqJDGwsa1mIw= +github.com/containerd/console v1.0.4 h1:F2g4+oChYvBTsASRTz8NP6iIAi97J3TtSAsLbIFn4ro= +github.com/containerd/console v1.0.4/go.mod h1:YynlIjWYF8myEu6sdkwKIvGQq+cOckRm6So2avqoYAk= +github.com/containerd/containerd v1.7.19 h1:/xQ4XRJ0tamDkdzrrBAUy/LE5nCcxFKdBm4EcPrSMEE= +github.com/containerd/containerd v1.7.19/go.mod h1:h4FtNYUUMB4Phr6v+xG89RYKj9XccvbNSCKjdufCrkc= +github.com/containerd/containerd/api v1.7.19 h1:VWbJL+8Ap4Ju2mx9c9qS1uFSB1OVYr5JJrW2yT5vFoA= +github.com/containerd/containerd/api v1.7.19/go.mod h1:fwGavl3LNwAV5ilJ0sbrABL44AQxmNjDRcwheXDb6Ig= +github.com/containerd/continuity v0.4.3 h1:6HVkalIp+2u1ZLH1J/pYX2oBVXlJZvh1X1A7bEZ9Su8= +github.com/containerd/continuity v0.4.3/go.mod h1:F6PTNCKepoxEaXLQp3wDAjygEnImnZ/7o4JzpodfroQ= +github.com/containerd/errdefs v0.1.0 h1:m0wCRBiu1WJT/Fr+iOoQHMQS/eP5myQ8lCv4Dz5ZURM= +github.com/containerd/errdefs v0.1.0/go.mod h1:YgWiiHtLmSeBrvpw+UfPijzbLaB77mEG1WwJTDETIV0= +github.com/containerd/fifo v1.1.0 h1:4I2mbh5stb1u6ycIABlBw9zgtlK8viPI9QkQNRQEEmY= +github.com/containerd/fifo v1.1.0/go.mod h1:bmC4NWMbXlt2EZ0Hc7Fx7QzTFxgPID13eH0Qu+MAb2o= +github.com/containerd/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I= +github.com/containerd/log v0.1.0/go.mod h1:VRRf09a7mHDIRezVKTRCrOq78v577GXq3bSa3EhrzVo= +github.com/containerd/nydus-snapshotter v0.13.7 h1:x7DHvGnzJOu1ZPwPYkeOPk5MjZZYbdddygEjaSDoFTk= +github.com/containerd/nydus-snapshotter v0.13.7/go.mod h1:VPVKQ3jmHFIcUIV2yiQ1kImZuBFS3GXDohKs9mRABVE= +github.com/containerd/platforms v0.2.1 h1:zvwtM3rz2YHPQsF2CHYM8+KtB5dvhISiXh5ZpSBQv6A= +github.com/containerd/platforms v0.2.1/go.mod h1:XHCb+2/hzowdiut9rkudds9bE5yJ7npe7dG/wG+uFPw= +github.com/containerd/stargz-snapshotter v0.15.1 h1:fpsP4kf/Z4n2EYnU0WT8ZCE3eiKDwikDhL6VwxIlgeA= +github.com/containerd/stargz-snapshotter/estargz v0.15.1 h1:eXJjw9RbkLFgioVaTG+G/ZW/0kEe2oEKCdS/ZxIyoCU= +github.com/containerd/stargz-snapshotter/estargz v0.15.1/go.mod h1:gr2RNwukQ/S9Nv33Lt6UC7xEx58C+LHRdoqbEKjz1Kk= +github.com/containerd/ttrpc v1.2.5 h1:IFckT1EFQoFBMG4c3sMdT8EP3/aKfumK1msY+Ze4oLU= +github.com/containerd/ttrpc v1.2.5/go.mod h1:YCXHsb32f+Sq5/72xHubdiJRQY9inL4a4ZQrAbN1q9o= +github.com/containerd/typeurl/v2 v2.1.1 h1:3Q4Pt7i8nYwy2KmQWIw2+1hTvwTE/6w9FqcttATPO/4= +github.com/containerd/typeurl/v2 v2.1.1/go.mod h1:IDp2JFvbwZ31H8dQbEIY7sDl2L3o3HZj1hsSQlywkQ0= +github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= +github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= +github.com/creack/pty v1.1.21 h1:1/QdRyBaHHJP61QkWMXlOIBfsgdDeeKfK8SYVUWJKf0= +github.com/creack/pty v1.1.21/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4= +github.com/dave/jennifer v1.6.1/go.mod h1:nXbxhEmQfOZhWml3D1cDK5M1FLnMSozpbFN/m3RmGZc= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/denisenkom/go-mssqldb v0.0.0-20191124224453-732737034ffd/go.mod h1:xbL0rPBG9cCiLr28tMa8zpbdarY27NDyej4t/EjAShU= +github.com/denisenkom/go-mssqldb v0.0.0-20191128021309-1d7a30a10f73 h1:OGNva6WhsKst5OZf7eZOklDztV3hwtTHovdrLHV+MsA= +github.com/denisenkom/go-mssqldb v0.0.0-20191128021309-1d7a30a10f73/go.mod h1:xbL0rPBG9cCiLr28tMa8zpbdarY27NDyej4t/EjAShU= +github.com/dgraph-io/badger/v4 v4.2.0 h1:kJrlajbXXL9DFTNuhhu9yCx7JJa4qpYWxtE8BzuWsEs= +github.com/dgraph-io/badger/v4 v4.2.0/go.mod h1:qfCqhPoWDFJRx1gp5QwwyGo8xk1lbHUxvK9nK0OGAak= +github.com/dgraph-io/ristretto v0.1.1 h1:6CWw5tJNgpegArSHpNHJKldNeq03FQCwYvfMVWajOK8= +github.com/dgraph-io/ristretto v0.1.1/go.mod h1:S1GPSBCYCIhmVNfcth17y2zZtQT6wzkzgwUve0VDWWA= +github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= +github.com/dgryski/go-farm v0.0.0-20190423205320-6a90982ecee2 h1:tdlZCpZ/P9DhczCTSixgIKmwPv6+wP5DGjqLYw5SUiA= +github.com/dgryski/go-farm v0.0.0-20190423205320-6a90982ecee2/go.mod h1:SqUrOPUnsFjfmXRMNPybcSiG0BgUW2AuFH8PAnS2iTw= +github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk= +github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E= +github.com/docker/buildx v0.16.0 h1:LurEflyb6BBoLtDwJY1dw9dLHKzEgGvCjAz67QI0xO0= +github.com/docker/buildx v0.16.0/go.mod h1:4xduW7BOJ2B11AyORKZFDKjF6Vcb4EgTYnV2nunxv9I= +github.com/docker/cli v27.0.3+incompatible h1:usGs0/BoBW8MWxGeEtqPMkzOY56jZ6kYlSN5BLDioCQ= +github.com/docker/cli v27.0.3+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8= +github.com/docker/compose/v2 v2.29.0 h1:qPBhzfjT2zkxUXuu+TcbQq292bPpB0ozzVHot2w2IN0= +github.com/docker/compose/v2 v2.29.0/go.mod h1:95QFO8lue3WJmLUDSdOLBkm7KdGhcG6U+RvVxrQIzOo= +github.com/docker/distribution v2.7.1+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w= +github.com/docker/distribution v2.8.3+incompatible h1:AtKxIZ36LoNK51+Z6RpzLpddBirtxJnzDrHLEKxTAYk= +github.com/docker/distribution v2.8.3+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w= +github.com/docker/docker v27.1.0+incompatible h1:rEHVQc4GZ0MIQKifQPHSFGV/dVgaZafgRf8fCPtDYBs= +github.com/docker/docker v27.1.0+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= +github.com/docker/docker-credential-helpers v0.8.2 h1:bX3YxiGzFP5sOXWc3bTPEXdEaZSeVMrFgOr3T+zrFAo= +github.com/docker/docker-credential-helpers v0.8.2/go.mod h1:P3ci7E3lwkZg6XiHdRKft1KckHiO9a2rNtyFbZ/ry9M= +github.com/docker/go v1.5.1-1.0.20160303222718-d30aec9fd63c h1:lzqkGL9b3znc+ZUgi7FlLnqjQhcXxkNM/quxIjBVMD0= +github.com/docker/go v1.5.1-1.0.20160303222718-d30aec9fd63c/go.mod h1:CADgU4DSXK5QUlFslkQu2yW2TKzFZcXq/leZfM0UH5Q= +github.com/docker/go-connections v0.4.0/go.mod h1:Gbd7IOopHjR8Iph03tsViu4nIes5XhDvyHbTtUxmeec= +github.com/docker/go-connections v0.5.0 h1:USnMq7hx7gwdVZq1L49hLXaFtUdTADjXGp+uj1Br63c= +github.com/docker/go-connections v0.5.0/go.mod h1:ov60Kzw0kKElRwhNs9UlUHAE/F9Fe6GLaXnqyDdmEXc= +github.com/docker/go-events v0.0.0-20190806004212-e31b211e4f1c h1:+pKlWGMw7gf6bQ+oDZB4KHQFypsfjYlq/C4rfL7D3g8= +github.com/docker/go-events v0.0.0-20190806004212-e31b211e4f1c/go.mod h1:Uw6UezgYA44ePAFQYUehOuCzmy5zmg/+nl2ZfMWGkpA= +github.com/docker/go-metrics v0.0.0-20180209012529-399ea8c73916/go.mod h1:/u0gXw0Gay3ceNrsHubL3BtdOL2fHf93USgMTe0W5dI= +github.com/docker/go-metrics v0.0.1 h1:AgB/0SvBxihN0X8OR4SjsblXkbMvalQ8cjmtKQ2rQV8= +github.com/docker/go-metrics v0.0.1/go.mod h1:cG1hvH2utMXtqgqqYE9plW6lDxS3/5ayHzueweSI3Vw= +github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4= +github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= +github.com/docker/libtrust v0.0.0-20160708172513-aabc10ec26b7 h1:UhxFibDNY/bfvqU5CAUmr9zpesgbU6SWc8/B4mflAE4= +github.com/docker/libtrust v0.0.0-20160708172513-aabc10ec26b7/go.mod h1:cyGadeNEkKy96OOhEzfZl+yxihPEzKnqJwvfuSUqbZE= +github.com/dsnet/compress v0.0.1 h1:PlZu0n3Tuv04TzpfPbrnI0HW/YwodEXDS+oPKahKF0Q= +github.com/dsnet/compress v0.0.1/go.mod h1:Aw8dCMJ7RioblQeTqt88akK31OvO8Dhf5JflhBbQEHo= +github.com/dsnet/golib v0.0.0-20171103203638-1ea166775780/go.mod h1:Lj+Z9rebOhdfkVLjJ8T6VcRQv3SXugXy999NBtR9aFY= +github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= +github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= +github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= +github.com/dvsekhvalnov/jose2go v0.0.0-20170216131308-f21a8cedbbae/go.mod h1:7BvyPhdbLxMXIYTFPLsyJRFMsKmOZnQmzh6Gb+uquuM= +github.com/emicklei/go-restful/v3 v3.11.0 h1:rAQeMHw1c7zTmncogyy8VvRZwtkmkZ4FxERmMY4rD+g= +github.com/emicklei/go-restful/v3 v3.11.0/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= +github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= +github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= +github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= +github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= +github.com/erikstmartin/go-testdb v0.0.0-20160219214506-8d10e4a1bae5 h1:Yzb9+7DPaBjB8zlTR87/ElzFsnQfuHnVUVqpZZIcV5Y= +github.com/erikstmartin/go-testdb v0.0.0-20160219214506-8d10e4a1bae5/go.mod h1:a2zkGnVExMxdzMo3M0Hi/3sEU+cWnZpSni0O6/Yb/P0= +github.com/fatih/color v1.16.0 h1:zmkK9Ngbjj+K0yRhTVONQh1p/HknKYSlNT+vZCzyokM= +github.com/fatih/color v1.16.0/go.mod h1:fL2Sau1YI5c0pdGEVCbKQbLXB6edEj1ZgiY4NijnWvE= +github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= +github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= +github.com/fogleman/gg v1.2.1-0.20190220221249-0403632d5b90/go.mod h1:R/bRT+9gY/C5z7JzPU0zXsXHKM4/ayA+zqcVNZzPa1k= +github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= +github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= +github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= +github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA= +github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM= +github.com/fvbommel/sortorder v1.0.2 h1:mV4o8B2hKboCdkJm+a7uX/SIpZob4JzUpc5GGnM45eo= +github.com/fvbommel/sortorder v1.0.2/go.mod h1:uk88iVf1ovNn1iLfgUVU2F9o5eO30ui720w+kxuqRs0= +github.com/gabriel-vasile/mimetype v1.4.3 h1:in2uUcidCuFcDKtdcBxlR0rJ1+fsokWf+uqxgUFjbI0= +github.com/gabriel-vasile/mimetype v1.4.3/go.mod h1:d8uq/6HKRL6CGdk+aubisF/M5GcPfT7nKyLpA0lbSSk= +github.com/gin-contrib/gzip v1.0.1 h1:HQ8ENHODeLY7a4g1Au/46Z92bdGFl74OhxcZble9WJE= +github.com/gin-contrib/gzip v1.0.1/go.mod h1:njt428fdUNRvjuJf16tZMYZ2Yl+WQB53X5wmhDwXvC4= +github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE= +github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI= +github.com/gin-gonic/gin v1.10.0 h1:nTuyha1TYqgedzytsKYqna+DfLos46nTv2ygFy86HFU= +github.com/gin-gonic/gin v1.10.0/go.mod h1:4PMNQiOhvDRa013RKVbsiNwoyezlm2rm0uX/T7kzp5Y= +github.com/glebarez/go-sqlite v1.21.2 h1:3a6LFC4sKahUunAmynQKLZceZCOzUthkRkEAl9gAXWo= +github.com/glebarez/go-sqlite v1.21.2/go.mod h1:sfxdZyhQjTM2Wry3gVYWaW072Ri1WMdWJi0k6+3382k= +github.com/glebarez/sqlite v1.11.0 h1:wSG0irqzP6VurnMEpFGer5Li19RpIRi2qvQz++w0GMw= +github.com/glebarez/sqlite v1.11.0/go.mod h1:h8/o8j5wiAsqSPoWELDUdJXhjAhsVliSn7bWZjOhrgQ= +github.com/go-acme/lego/v4 v4.17.4 h1:h0nePd3ObP6o7kAkndtpTzCw8shOZuWckNYeUQwo36Q= +github.com/go-acme/lego/v4 v4.17.4/go.mod h1:dU94SvPNqimEeb7EVilGGSnS0nU1O5Exir0pQ4QFL4U= +github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= +github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= +github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= +github.com/go-gormigrate/gormigrate/v2 v2.1.2 h1:F/d1hpHbRAvKezziV2CC5KUE82cVe9zTgHSBoOOZ4CY= +github.com/go-gormigrate/gormigrate/v2 v2.1.2/go.mod h1:9nHVX6z3FCMCQPA7PThGcA55t22yKQfK/Dnsf5i7hUo= +github.com/go-ini/ini v1.67.0 h1:z6ZrTEZqSWOTyH2FlglNbNgARyHG8oLW9gMELqKr06A= +github.com/go-ini/ini v1.67.0/go.mod h1:ByCAeIL28uOIIG0E3PJtZPDL8WnHpFKFOtgjp+3Ies8= +github.com/go-jose/go-jose/v4 v4.0.2 h1:R3l3kkBds16bO7ZFAEEcofK0MkrAJt3jlJznWZG0nvk= +github.com/go-jose/go-jose/v4 v4.0.2/go.mod h1:WVf9LFMHh/QVrmqrOfqun0C45tMe3RoiKJMPvgWwLfY= +github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= +github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= +github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= +github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= +github.com/go-logr/logr v1.3.0/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/logr v1.4.1 h1:pKouT5E8xu9zeFC39JXRDukb6JFQPXM5p5I91188VAQ= +github.com/go-logr/logr v1.4.1/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= +github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= +github.com/go-ole/go-ole v1.2.6 h1:/Fpf6oFPoeFik9ty7siob0G6Ke8QvQEuVcuChpwXzpY= +github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0= +github.com/go-openapi/jsonpointer v0.19.3/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg= +github.com/go-openapi/jsonpointer v0.19.5/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg= +github.com/go-openapi/jsonpointer v0.19.6 h1:eCs3fxoIi3Wh6vtgmLTOjdhSpiqphQ+DaPn38N2ZdrE= +github.com/go-openapi/jsonpointer v0.19.6/go.mod h1:osyAmYz/mB/C3I+WsTTSgw1ONzaLJoLCyoi6/zppojs= +github.com/go-openapi/jsonreference v0.19.6/go.mod h1:diGHMEHg2IqXZGKxqyvWdfWU/aim5Dprw5bqpKkTvns= +github.com/go-openapi/jsonreference v0.20.2 h1:3sVjiK66+uXK/6oQ8xgcRKcFgQ5KXa2KvnJRumpMGbE= +github.com/go-openapi/jsonreference v0.20.2/go.mod h1:Bl1zwGIM8/wsvqjsOQLJ/SH+En5Ap4rVB5KVcIDZG2k= +github.com/go-openapi/spec v0.20.4 h1:O8hJrt0UMnhHcluhIdUgCLRWyM2x7QkBXRvOs7m+O1M= +github.com/go-openapi/spec v0.20.4/go.mod h1:faYFR1CvsJZ0mNsmsphTMSoRrNV3TEDoAM7FOEWeq8I= +github.com/go-openapi/swag v0.19.5/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk= +github.com/go-openapi/swag v0.19.15/go.mod h1:QYRuS/SOXUCsnplDa677K7+DxSOj6IPNl/eQntq43wQ= +github.com/go-openapi/swag v0.22.3 h1:yMBqmnQ0gyZvEb/+KzuWZOXgllrXT4SADYbvDaXHv/g= +github.com/go-openapi/swag v0.22.3/go.mod h1:UzaqsxGiab7freDnrUUra0MwWfN/q7tE4j+VcZ0yl14= +github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= +github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s= +github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= +github.com/go-playground/locales v0.13.0/go.mod h1:taPMhCMXrRLJO55olJkUXHZBHCxTMfnGwq/HNwmWNS8= +github.com/go-playground/locales v0.14.0/go.mod h1:sawfccIbzZTqEDETgFXqTho0QybSa7l++s0DH+LDiLs= +github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA= +github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY= +github.com/go-playground/universal-translator v0.17.0/go.mod h1:UkSxE5sNxxRwHyU+Scu5vgOQjsIJAF8j9muTVoKLVtA= +github.com/go-playground/universal-translator v0.18.0/go.mod h1:UvRDBj+xPUEGrFYl+lu/H90nyDXpg0fqeB/AQUGNTVA= +github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY= +github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= +github.com/go-playground/validator/v10 v10.8.0/go.mod h1:9JhgTzTaE31GZDpH/HSvHiRJrJ3iKAgqqH0Bl/Ocjdk= +github.com/go-playground/validator/v10 v10.22.0 h1:k6HsTZ0sTnROkhS//R0O+55JgM8C4Bx7ia+JlgcnOao= +github.com/go-playground/validator/v10 v10.22.0/go.mod h1:dbuPbCMFw/DrkbEynArYaCwl3amGuJotoKCe95atGMM= +github.com/go-redis/redis v6.15.9+incompatible h1:K0pv1D7EQUjfyoMql+r/jZqCLizCGKFlFgcHWWmHQjg= +github.com/go-redis/redis v6.15.9+incompatible/go.mod h1:NAIEuMOZ/fxfXJIrKDQDz8wamY7mA7PouImQ2Jvg6kA= +github.com/go-sql-driver/mysql v1.3.0/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w= +github.com/go-sql-driver/mysql v1.5.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg= +github.com/go-sql-driver/mysql v1.8.1 h1:LedoTUt/eveggdHS9qUFC1EFSa8bU2+1pZjSRpvNJ1Y= +github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg= +github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= +github.com/go-viper/mapstructure/v2 v2.0.0 h1:dhn8MZ1gZ0mzeodTG3jt5Vj/o87xZKuNAprG2mQfMfc= +github.com/go-viper/mapstructure/v2 v2.0.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= +github.com/goccy/go-json v0.10.3 h1:KZ5WoDbxAIgm2HNbYckL0se1fHD6rz5j4ywS6ebzDqA= +github.com/goccy/go-json v0.10.3/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M= +github.com/gofrs/flock v0.8.1/go.mod h1:F1TvTiK9OcQqauNUHlbJvyl9Qa1QvF/gOUDKA14jxHU= +github.com/gofrs/flock v0.12.0 h1:xHW8t8GPAiGtqz7KxiSqfOEXwpOaqhpYZrTE2MQBgXY= +github.com/gofrs/flock v0.12.0/go.mod h1:FirDy1Ing0mI2+kB6wk+vyyAH+e6xiE+EYA0jnzV9jc= +github.com/gogo/googleapis v1.4.1 h1:1Yx4Myt7BxzvUr5ldGSbwYiZG6t9wGBZ+8/fX3Wvtq0= +github.com/gogo/googleapis v1.4.1/go.mod h1:2lpHqI5OcWCtVElxXnPt+s8oJvMpySlOyM6xDCrzib4= +github.com/gogo/protobuf v1.0.0/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= +github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= +github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= +github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= +github.com/goh-chunlin/go-onedrive v1.1.1 h1:HGtHk5iG0MZ92zYUtaY04czfZPBIJUr12UuFc+PW8m4= +github.com/goh-chunlin/go-onedrive v1.1.1/go.mod h1:N8qIGHD7tryO734epiBKk5oXcpGwxKET/u3LuBHciTs= +github.com/goji/httpauth v0.0.0-20160601135302-2da839ab0f4d/go.mod h1:nnjvkQ9ptGaCkuDUx6wNykzzlUixGxvkme+H/lnzb+A= +github.com/golang-jwt/jwt/v4 v4.5.0 h1:7cYmW1XlMY7h7ii7UhUyChSgS5wUJEnm9uZVTGqOWzg= +github.com/golang-jwt/jwt/v4 v4.5.0/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0= +github.com/golang-sql/civil v0.0.0-20190719163853-cb61b32ac6fe h1:lXe2qZdvpiX5WZkZR4hgp4KJVfY3nMkvmwbVkpv1rVY= +github.com/golang-sql/civil v0.0.0-20190719163853-cb61b32ac6fe/go.mod h1:8vg3r2VgvsThLBIFL93Qb5yWzgyZWhEmBwUJWevAkK0= +github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0/go.mod h1:E/TSTwGwJL78qG/PmXZO1EjYhfJinVAhrmmHX6Z8B9k= +github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= +github.com/golang/glog v1.2.0 h1:uCdmnmatrKCgMBlM4rMuJZWOkPDqdbZPnrMXDY4gI68= +github.com/golang/glog v1.2.0/go.mod h1:6AhwSGph0fcJtXVM/PEHPqZlFeoLxhs7/t5UDAwmO+w= +github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE= +github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y= +github.com/golang/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= +github.com/golang/mock v1.4.1/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= +github.com/golang/mock v1.4.3/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= +github.com/golang/mock v1.4.4 h1:l75CXGRSwbaYNpl/Z2X1XIIAMSCquvXgpVZDhwEIJsc= +github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4= +github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= +github.com/golang/protobuf v1.3.4/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= +github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk= +github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= +github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= +github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= +github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= +github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= +github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8= +github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= +github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= +github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= +github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= +github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= +github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM= +github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= +github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= +github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= +github.com/google/certificate-transparency-go v1.0.10-0.20180222191210-5ab67e519c93 h1:jc2UWq7CbdszqeH6qu1ougXMIUBfSy8Pbh/anURYbGI= +github.com/google/certificate-transparency-go v1.0.10-0.20180222191210-5ab67e519c93/go.mod h1:QeJfpSbVSfYc7RgB3gJFj9cbuQMMchQxrWXz8Ruopmg= +github.com/google/flatbuffers v1.12.1 h1:MVlul7pQNoDzWRLTw5imwYsl+usrS1TXG2H4jg6ImGw= +github.com/google/flatbuffers v1.12.1/go.mod h1:1AeVuKshWv4vARoZatz6mlQ0JxURH0Kv5+zNeJKJCa8= +github.com/google/gnostic-models v0.6.8 h1:yo/ABAfM5IMRsS1VnXjTBvUb61tFIHozhlYvRgGre9I= +github.com/google/gnostic-models v0.6.8/go.mod h1:5n7qKqH0f5wFt+aWF8CW6pZLLNOfYuF5OpfBSENuI8U= +github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= +github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.4.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= +github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck= +github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8= +github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU= +github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0= +github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= +github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= +github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= +github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= +github.com/google/pprof v0.0.0-20191218002539-d4f498aebedc/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20200212024743-f11f1df84d12/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20200229191704-1ebb73c60ed3/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20200430221834-fc25d7d30c6d/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20200708004538-1a94d8640e99/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20240424215950-a892ee059fd6 h1:k7nVchz72niMH6YLQNvHSdIE7iqsQxK1P41mySCvssg= +github.com/google/pprof v0.0.0-20240424215950-a892ee059fd6/go.mod h1:kf6iHlnVGwgKolg33glAes7Yg/8iWP8ukqeldJSO7jw= +github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= +github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 h1:El6M4kTTCOh6aBiKaUGG7oYTSPP8MxqL4YI3kZKwcP4= +github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510/go.mod h1:pupxD2MaaD3pAXIBCelhxNneeOaAeabZDe5s4K6zSpQ= +github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= +github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= +github.com/gorilla/mux v1.7.0/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs= +github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY= +github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ= +github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg= +github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= +github.com/grafov/m3u8 v0.12.0/go.mod h1:nqzOkfBiZJENr52zTVd/Dcl03yzphIMbJqkXGu+u080= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.16.0 h1:YBftPWNWd4WwGqtY2yeZL2ef8rHAxPBD8KFhJpmcqms= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.16.0/go.mod h1:YN5jB8ie0yfIUg6VvR9Kz84aCaG7AsGZnLjhHbUqwPg= +github.com/h2non/filetype v1.1.1 h1:xvOwnXKAckvtLWsN398qS9QhlxlnVXBjXBydK2/UFB4= +github.com/h2non/filetype v1.1.1/go.mod h1:319b3zT68BvV+WRj7cwy856M2ehB3HqNOt6sy1HndBY= +github.com/hailocab/go-hostpool v0.0.0-20160125115350-e80d13ce29ed h1:5upAirOpQc1Q53c0bnx2ufif5kANL7bfZWcc6VJWJd8= +github.com/hailocab/go-hostpool v0.0.0-20160125115350-e80d13ce29ed/go.mod h1:tMWxXQ9wFIaZeTI9F+hmhFiGpFmhOHzyShyFUhRm0H4= +github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= +github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I= +github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= +github.com/hashicorp/go-cleanhttp v0.5.2 h1:035FKYIWjmULyFRBKPs8TBQoi0x6d9G4xc9neXJWAZQ= +github.com/hashicorp/go-cleanhttp v0.5.2/go.mod h1:kO/YDlP8L1346E6Sodw+PrpBSV4/SoxCXGY6BqNFT48= +github.com/hashicorp/go-hclog v1.6.3 h1:Qr2kF+eVWjTiYmU7Y31tYlP1h0q/X3Nl3tPGdaB11/k= +github.com/hashicorp/go-hclog v1.6.3/go.mod h1:W4Qnvbt70Wk/zYJryRzDRU/4r0kIg0PVHBcfoyhpF5M= +github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo= +github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM= +github.com/hashicorp/go-retryablehttp v0.7.7 h1:C8hUCYzor8PIfXHa4UrZkU4VvK8o9ISHxT2Q8+VepXU= +github.com/hashicorp/go-retryablehttp v0.7.7/go.mod h1:pkQpWZeYWskR+D1tR2O5OcBFOxfA7DoAO6xtkuQnHTk= +github.com/hashicorp/go-version v1.7.0 h1:5tqGy27NaOTB8yJKUZELlFAS/LTKJkrmONwQKeRZfjY= +github.com/hashicorp/go-version v1.7.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= +github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= +github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= +github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= +github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= +github.com/hpcloud/tail v1.0.0 h1:nfCOvKYfkgYP8hkirhJocXT2+zOD8yUNjXaWfTlyFKI= +github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= +github.com/iancoleman/strcase v0.3.0/go.mod h1:iwCmte+B7n89clKwxIoIXy/HfoL7AsD47ZCWhYzw7ho= +github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= +github.com/imdario/mergo v0.3.16 h1:wwQJbIsHYGMUyLSPrEq1CT16AhnhNJQ51+4fdHUnCl4= +github.com/imdario/mergo v0.3.16/go.mod h1:WBLT9ZmE3lPoWsEzCh9LPo3TiwVN+ZKEjmz+hD27ysY= +github.com/in-toto/in-toto-golang v0.5.0 h1:hb8bgwr0M2hGdDsLjkJ3ZqJ8JFLL/tgYdAxF/XEFBbY= +github.com/in-toto/in-toto-golang v0.5.0/go.mod h1:/Rq0IZHLV7Ku5gielPT4wPHJfH1GdHMCq8+WPxw8/BE= +github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= +github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= +github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= +github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM= +github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg= +github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a h1:bbPeKD0xmW/Y25WS6cokEszi5g+S0QxI/d45PkRi7Nk= +github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM= +github.com/jackc/pgx/v5 v5.6.0 h1:SWJzexBzPL5jb0GEsrPMLIsi/3jOo7RHlzTjcAeDrPY= +github.com/jackc/pgx/v5 v5.6.0/go.mod h1:DNZ/vlrUnhWCoFGxHAG8U2ljioxukquj7utPDgtQdTw= +github.com/jackc/puddle/v2 v2.2.1 h1:RhxXJtFG022u4ibrCSMSiu5aOq1i77R3OHKNJj77OAk= +github.com/jackc/puddle/v2 v2.2.1/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4= +github.com/jinzhu/copier v0.4.0 h1:w3ciUoD19shMCRargcpm0cm91ytaBhDvuRpz1ODO/U8= +github.com/jinzhu/copier v0.4.0/go.mod h1:DfbEm0FYsaqBcKcFuvmOZb218JkPGtvSHsKg8S8hyyg= +github.com/jinzhu/gorm v0.0.0-20170222002820-5409931a1bb8/go.mod h1:Vla75njaFJ8clLU1W44h34PjIkijhjHIYnZxMqCdxqo= +github.com/jinzhu/gorm v1.9.16 h1:+IyIjPEABKRpsu/F8OvDPy9fyQlgsg2luMV2ZIH5i5o= +github.com/jinzhu/gorm v1.9.16/go.mod h1:G3LB3wezTOWM2ITLzPxEXgSkOXAntiLHS7UdBefADcs= +github.com/jinzhu/inflection v0.0.0-20170102125226-1c35d901db3d/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc= +github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E= +github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc= +github.com/jinzhu/now v1.0.1/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= +github.com/jinzhu/now v1.1.1/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= +github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ= +github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= +github.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9YPoQUg= +github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo= +github.com/jmespath/go-jmespath/internal/testify v1.5.1 h1:shLQSRRSCCPj3f2gpwzGwWFoC7ycTf1rcQZHOlsJ6N8= +github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U= +github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= +github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= +github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= +github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= +github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= +github.com/json-iterator/go v1.1.7/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= +github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= +github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= +github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= +github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk= +github.com/juju/loggo v0.0.0-20190526231331-6e530bcce5d8/go.mod h1:vgyd7OREkbtVEN/8IXZe5Ooef3LQePvuBm9UWj6ZL8U= +github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= +github.com/jung-kurt/gofpdf v1.0.3-0.20190309125859-24315acbbda5/go.mod h1:7Id9E/uU8ce6rXgefFLlgrJj/GYY22cpxn+r32jIOes= +github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= +github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= +github.com/klauspost/compress v1.4.1/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A= +github.com/klauspost/compress v1.17.9 h1:6KIumPrER1LHsvBVuDa0r5xaG0Es51mhhB9BQB2qeMA= +github.com/klauspost/compress v1.17.9/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw= +github.com/klauspost/cpuid v1.2.0/go.mod h1:Pj4uuM528wm8OyEC2QMXAi2YiTZ96dNQPGgoMS4s3ek= +github.com/klauspost/cpuid/v2 v2.0.1/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= +github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= +github.com/klauspost/cpuid/v2 v2.2.8 h1:+StwCXwm9PdpiEkPyzBXIy+M9KUb4ODm0Zarf1kS5BM= +github.com/klauspost/cpuid/v2 v2.2.8/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws= +github.com/klauspost/pgzip v1.2.5 h1:qnWYvvKqedOF2ulHpMG72XQol4ILEJ8k2wwRl/Km8oE= +github.com/klauspost/pgzip v1.2.5/go.mod h1:Ch1tH69qFZu15pkjo5kYi6mth2Zzwzt50oCQKQE9RUs= +github.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M= +github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= +github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= +github.com/kr/fs v0.1.0 h1:Jskdu9ieNAYnjxsi0LbQp1ulIKZV1LAFgK1tWhpZgl8= +github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg= +github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= +github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/leodido/go-urn v1.2.1/go.mod h1:zt4jvISO2HfUBqxjfIshjdMTYS56ZS/qv49ictyFfxY= +github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ= +github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI= +github.com/lib/pq v0.0.0-20150723085316-0dad96c0b94f/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= +github.com/lib/pq v1.1.1 h1:sJZmqHoEaY7f+NPP8pgLB/WxulyR3fewgCM2qaSlBb4= +github.com/lib/pq v1.1.1/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= +github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 h1:6E+4a0GO5zZEnZ81pIr0yLvtUWk2if982qA3F3QD6H4= +github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0/go.mod h1:zJYVVT2jmtg6P3p1VtQj7WsuWi/y4VnjVBn7F8KPB3I= +github.com/magiconair/properties v1.5.3/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= +github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY= +github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0= +github.com/mailru/easyjson v0.0.0-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= +github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= +github.com/mailru/easyjson v0.7.6/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= +github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= +github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= +github.com/matishsiao/goInfo v0.0.0-20210923090445-da2e3fa8d45f h1:B0OD7nYl2FPQEVrw8g2uyc1lGEzNbvrKh7fspGZcbvY= +github.com/matishsiao/goInfo v0.0.0-20210923090445-da2e3fa8d45f/go.mod h1:aEt7p9Rvh67BYApmZwNDPpgircTO2kgdmDUoF/1QmwA= +github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= +github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= +github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= +github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/mattn/go-shellwords v1.0.12 h1:M2zGm7EW6UQJvDeQxo4T51eKPurbeFbe8WtebGE2xrk= +github.com/mattn/go-shellwords v1.0.12/go.mod h1:EZzvwXDESEeg03EKmM+RmDnNOPKG4lLtQsUlTZDWQ8Y= +github.com/mattn/go-sqlite3 v1.6.0/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc= +github.com/mattn/go-sqlite3 v1.14.0/go.mod h1:JIl7NbARA7phWnGvh0LKTyg7S9BA+6gx71ShQilpsus= +github.com/mattn/go-sqlite3 v1.14.16 h1:yOQRA0RpS5PFz/oikGwBEqvAWhWg5ufRz4ETLjwpU1Y= +github.com/mattn/go-sqlite3 v1.14.16/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg= +github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= +github.com/matttproud/golang_protobuf_extensions v1.0.4 h1:mmDVorXM7PCGKw94cs5zkfA9PSy5pEvNWRP0ET0TIVo= +github.com/matttproud/golang_protobuf_extensions v1.0.4/go.mod h1:BSXmuO+STAnVfrANrmjBb36TMTDstsz7MSK+HVaYKv4= +github.com/mholt/archiver/v4 v4.0.0-alpha.8 h1:tRGQuDVPh66WCOelqe6LIGh0gwmfwxUrSSDunscGsRM= +github.com/mholt/archiver/v4 v4.0.0-alpha.8/go.mod h1:5f7FUYGXdJWUjESffJaYR4R60VhnHxb2X3T1teMyv5A= +github.com/miekg/dns v1.1.59 h1:C9EXc/UToRwKLhK5wKU/I4QVsBUc8kE6MkHBkeypWZs= +github.com/miekg/dns v1.1.59/go.mod h1:nZpewl5p6IvctfgrckopVx2OlSEHPRO/U4SYkRklrEk= +github.com/miekg/pkcs11 v1.0.2/go.mod h1:XsNlhZGX73bx86s2hdc/FuaLm2CPZJemRLMA+WTFxgs= +github.com/miekg/pkcs11 v1.1.1 h1:Ugu9pdy6vAYku5DEpVWVFPYnzV+bxB+iRdbuFSu7TvU= +github.com/miekg/pkcs11 v1.1.1/go.mod h1:XsNlhZGX73bx86s2hdc/FuaLm2CPZJemRLMA+WTFxgs= +github.com/minio/md5-simd v1.1.2 h1:Gdi1DZK69+ZVMoNHRXJyNcxrMA4dSxoYHZSQbirFg34= +github.com/minio/md5-simd v1.1.2/go.mod h1:MzdKDxYpY2BT9XQFocsiZf/NKVtR7nkE4RoEpN+20RM= +github.com/minio/minio-go/v7 v7.0.74 h1:fTo/XlPBTSpo3BAMshlwKL5RspXRv9us5UeHEGYCFe0= +github.com/minio/minio-go/v7 v7.0.74/go.mod h1:qydcVzV8Hqtj1VtEocfxbmVFa2siu6HGa+LDEPogjD8= +github.com/mitchellh/hashstructure/v2 v2.0.2 h1:vGKWl0YJqUNxE8d+h8f6NJLcCJrgbhC4NcD46KavDd4= +github.com/mitchellh/hashstructure/v2 v2.0.2/go.mod h1:MG3aRVU/N29oo/V/IhBX8GR/zz4kQkprJgF2EVszyDE= +github.com/mitchellh/mapstructure v0.0.0-20150613213606-2caf8efc9366/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= +github.com/mitchellh/mapstructure v1.4.3/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= +github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= +github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= +github.com/moby/buildkit v0.15.0 h1:vnZLThPr9JU6SvItctKoa6NfgPZ8oUApg/TCOaa/SVs= +github.com/moby/buildkit v0.15.0/go.mod h1:oN9S+8I7wF26vrqn9NuAF6dFSyGTfXvtiu9o1NlnnH4= +github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0= +github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo= +github.com/moby/locker v1.0.1 h1:fOXqR41zeveg4fFODix+1Ch4mj/gT0NE1XJbp/epuBg= +github.com/moby/locker v1.0.1/go.mod h1:S7SDdo5zpBK84bzzVlKr2V0hz+7x9hWbYC/kq7oQppc= +github.com/moby/patternmatcher v0.6.0 h1:GmP9lR19aU5GqSSFko+5pRqHi+Ohk1O69aFiKkVGiPk= +github.com/moby/patternmatcher v0.6.0/go.mod h1:hDPoyOpDY7OrrMDLaYoY3hf52gNCR/YOUYxkhApJIxc= +github.com/moby/sys/mountinfo v0.7.1 h1:/tTvQaSJRr2FshkhXiIpux6fQ2Zvc4j7tAhMTStAG2g= +github.com/moby/sys/mountinfo v0.7.1/go.mod h1:IJb6JQeOklcdMU9F5xQ8ZALD+CUr5VlGpwtX+VE0rpI= +github.com/moby/sys/sequential v0.5.0 h1:OPvI35Lzn9K04PBbCLW0g4LcFAJgHsvXsRyewg5lXtc= +github.com/moby/sys/sequential v0.5.0/go.mod h1:tH2cOOs5V9MlPiXcQzRC+eEyab644PWKGRYaaV5ZZlo= +github.com/moby/sys/signal v0.7.0 h1:25RW3d5TnQEoKvRbEKUGay6DCQ46IxAVTT9CUMgmsSI= +github.com/moby/sys/signal v0.7.0/go.mod h1:GQ6ObYZfqacOwTtlXvcmh9A26dVRul/hbOZn88Kg8Tg= +github.com/moby/sys/user v0.1.0 h1:WmZ93f5Ux6het5iituh9x2zAG7NFY9Aqi49jjE1PaQg= +github.com/moby/sys/user v0.1.0/go.mod h1:fKJhFOnsCN6xZ5gSfbM6zaHGgDJMrqt9/reuj4T7MmU= +github.com/moby/term v0.5.0 h1:xt8Q1nalod/v7BqbG21f8mQPqH+xAaC9C3N3wfWbVP0= +github.com/moby/term v0.5.0/go.mod h1:8FzsFHVUBGZdbDsJw/ot+X+d5HLUbvklYLJ9uGfcI3Y= +github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= +github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= +github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= +github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= +github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A= +github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc= +github.com/mozillazg/go-httpheader v0.2.1 h1:geV7TrjbL8KXSyvghnFm+NyTux/hxwueTSrwhe88TQQ= +github.com/mozillazg/go-httpheader v0.2.1/go.mod h1:jJ8xECTlalr6ValeXYdOF8fFUISeBAdw6E61aqQma60= +github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= +github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= +github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= +github.com/namedotcom/go v0.0.0-20180403034216-08470befbe04 h1:o6uBwrhM5C8Ll3MAAxrQxRHEu7FkapwTuI2WmL1rw4g= +github.com/namedotcom/go v0.0.0-20180403034216-08470befbe04/go.mod h1:5sN+Lt1CaY4wsPvgQH/jsuJi4XO2ssZbdsIizr4CVC8= +github.com/nicksnyder/go-i18n/v2 v2.4.0 h1:3IcvPOAvnCKwNm0TB0dLDTuawWEj+ax/RERNC+diLMM= +github.com/nicksnyder/go-i18n/v2 v2.4.0/go.mod h1:nxYSZE9M0bf3Y70gPQjN9ha7XNHX7gMc814+6wVyEI4= +github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= +github.com/nrdcg/dnspod-go v0.4.0 h1:c/jn1mLZNKF3/osJ6mz3QPxTudvPArXTjpkmYj0uK6U= +github.com/nrdcg/dnspod-go v0.4.0/go.mod h1:vZSoFSFeQVm2gWLMkyX61LZ8HI3BaqtHZWgPTGKr6KQ= +github.com/nrdcg/namesilo v0.2.1 h1:kLjCjsufdW/IlC+iSfAqj0iQGgKjlbUUeDJio5Y6eMg= +github.com/nrdcg/namesilo v0.2.1/go.mod h1:lwMvfQTyYq+BbjJd30ylEG4GPSS6PII0Tia4rRpRiyw= +github.com/nwaples/rardecode/v2 v2.0.0-beta.2 h1:e3mzJFJs4k83GXBEiTaQ5HgSc/kOK8q0rDaRO0MPaOk= +github.com/nwaples/rardecode/v2 v2.0.0-beta.2/go.mod h1:yntwv/HfMc/Hbvtq9I19D1n58te3h6KsqCf3GxyfBGY= +github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/ginkgo v1.12.0 h1:Iw5WCbBcaAAd0fpRb1c9r5YCylv4XDoCSigm1zLevwU= +github.com/onsi/ginkgo v1.12.0/go.mod h1:oUhWkIvk5aDxtKvDDuw8gItl8pKl42LzjC9KZE0HfGg= +github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY= +github.com/onsi/gomega v1.9.0/go.mod h1:Ho0h+IUsWyvy1OpqCwxlQ/21gkhVunqlU8fDGcoTdcA= +github.com/onsi/gomega v1.29.0 h1:KIA/t2t5UBzoirT4H9tsML45GEbo3ouUnBHsCfD2tVg= +github.com/onsi/gomega v1.29.0/go.mod h1:9sxs+SwGrKI0+PWe4Fxa9tFQQBG5xSsSbMXOI8PPpoQ= +github.com/opencontainers/go-digest v0.0.0-20170106003457-a6d0ee40d420/go.mod h1:cMLVZDEM3+U2I4VmLI6N8jQYUd2OVphdqWwCJHrFt2s= +github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= +github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= +github.com/opencontainers/image-spec v1.0.1/go.mod h1:BtxoFyWECRxE4U/7sNtV5W15zMzWCbyJoFRP3s7yZA0= +github.com/opencontainers/image-spec v1.1.0 h1:8SG7/vwALn54lVB/0yZ/MMwhFrPYtpEHQb2IpWsCzug= +github.com/opencontainers/image-spec v1.1.0/go.mod h1:W4s4sFTMaBeK1BQLXbG4AdM2szdn85PY75RI83NrTrM= +github.com/opencontainers/runtime-spec v1.2.0 h1:z97+pHb3uELt/yiAWD691HNHQIF07bE7dzrbT927iTk= +github.com/opencontainers/runtime-spec v1.2.0/go.mod h1:jwyrGlmzljRJv/Fgzds9SsS/C5hL+LL3ko9hs6T5lQ0= +github.com/opencontainers/selinux v1.11.0 h1:+5Zbo97w3Lbmb3PeqQtpmTkMwsW5nRI3YaLpt7tQ7oU= +github.com/opencontainers/selinux v1.11.0/go.mod h1:E5dMC3VPuVvVHDYmi78qvhJp8+M586T4DlDRYpFkyec= +github.com/opentracing/opentracing-go v1.1.0/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o= +github.com/opentracing/opentracing-go v1.2.1-0.20220228012449-10b1cf09e00b h1:FfH+VrHHk6Lxt9HdVS0PXzSXFyS2NbZKXv33FYPol0A= +github.com/opentracing/opentracing-go v1.2.1-0.20220228012449-10b1cf09e00b/go.mod h1:AC62GU6hc0BrNm+9RK9VSiwa/EUe1bkIeFORAMcHvJU= +github.com/pelletier/go-toml v1.9.5 h1:4yBQzkHv+7BHq2PQUZF3Mx0IYxG7LsP222s7Agd3ve8= +github.com/pelletier/go-toml v1.9.5/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c= +github.com/pelletier/go-toml/v2 v2.2.2 h1:aYUidT7k73Pcl9nb2gScu7NSrKCSHIDE89b3+6Wq+LM= +github.com/pelletier/go-toml/v2 v2.2.2/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h4qDAS4n929Rs= +github.com/pierrec/lz4/v4 v4.1.15 h1:MO0/ucJhngq7299dKLwIMtgTfbkoSPF6AoMYDd8Q4q0= +github.com/pierrec/lz4/v4 v4.1.15/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4= +github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= +github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/sftp v1.13.6 h1:JFZT4XbOU7l77xGSpOdW+pwIMqP044IyjXX6FGyEKFo= +github.com/pkg/sftp v1.13.6/go.mod h1:tz1ryNURKu77RL+GuCzmoJYxQczL3wLNNpPWagdg4Qk= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c h1:ncq/mPwQF4JjgDlrVEn3C11VoGHZN7m8qihwgMEtzYw= +github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE= +github.com/prometheus/client_golang v0.9.0-pre1.0.20180209125602-c332b6f63c06/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= +github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= +github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo= +github.com/prometheus/client_golang v1.1.0/go.mod h1:I1FGZT9+L76gKKOs5djB6ezCbFQP1xR9D75/vuwEF3g= +github.com/prometheus/client_golang v1.17.0 h1:rl2sfwZMtSthVU752MqfjQozy7blglC+1SOtjMAMh+Q= +github.com/prometheus/client_golang v1.17.0/go.mod h1:VeL+gMmOAxkS2IqfCq0ZmHSL+LjWfWDUmp1mBz9JgUY= +github.com/prometheus/client_model v0.0.0-20171117100541-99fa1f4be8e5/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= +github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= +github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/prometheus/client_model v0.5.0 h1:VQw1hfvPvk3Uv6Qf29VrPF32JB6rtbgI6cYPYQjL0Qw= +github.com/prometheus/client_model v0.5.0/go.mod h1:dTiFglRmd66nLR9Pv9f0mZi7B7fk5Pm3gvsjB5tr+kI= +github.com/prometheus/common v0.0.0-20180110214958-89604d197083/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro= +github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= +github.com/prometheus/common v0.6.0/go.mod h1:eBmuwkDJBwy6iBfxCBob6t6dR6ENT/y+J+Zk0j9GMYc= +github.com/prometheus/common v0.44.0 h1:+5BrQJwiBB9xsMygAB3TNvpQKOwlkc25LbISbrdOOfY= +github.com/prometheus/common v0.44.0/go.mod h1:ofAIvZbQ1e/nugmZGz4/qCb9Ap1VoSTIO7x0VV9VvuY= +github.com/prometheus/procfs v0.0.0-20180125133057-cb4147076ac7/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= +github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= +github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= +github.com/prometheus/procfs v0.0.3/go.mod h1:4A/X28fw3Fc593LaREMrKMqOKvUAntwMDaekg4FpcdQ= +github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc= +github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk= +github.com/qiniu/dyn v1.3.0/go.mod h1:E8oERcm8TtwJiZvkQPbcAh0RL8jO1G0VXJMW3FAWdkk= +github.com/qiniu/go-sdk/v7 v7.21.1 h1:D/IjVOlg5pTw0jeDjqTo6H5QM73Obb1AYfPOHmIFN+Q= +github.com/qiniu/go-sdk/v7 v7.21.1/go.mod h1:8EM2awITynlem2VML2dXGHkMYP2UyECsGLOdp6yMpco= +github.com/qiniu/x v1.10.5/go.mod h1:03Ni9tj+N2h2aKnAz+6N0Xfl8FwMEDRC2PAlxekASDs= +github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= +github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE= +github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= +github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs= +github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro= +github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= +github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc= +github.com/rogpeppe/go-internal v1.8.0/go.mod h1:WmiCO8CzOY8rg0OYDC4/i/2WRWAB6poM+XZ2dLUbcbE= +github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ= +github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog= +github.com/rs/xid v1.5.0 h1:mKX4bl4iPYJtEIxp6CYiUuLQ/8DYMoz0PUdtGgMFRVc= +github.com/rs/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg= +github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/rwcarlsen/goexif v0.0.0-20190401172101-9e8deecbddbd/go.mod h1:hPqNNc0+uJM6H+SuU8sEs5K5IQeKccPqeSjfgcKGgPk= +github.com/sagikazarmark/locafero v0.4.0 h1:HApY1R9zGo4DBgr7dqsTH/JJxLTTsOt7u6keLGt6kNQ= +github.com/sagikazarmark/locafero v0.4.0/go.mod h1:Pe1W6UlPYUk/+wc/6KFhbORCfqzgYEpgQ3O5fPuL3H4= +github.com/sagikazarmark/slog-shim v0.1.0 h1:diDBnUNK9N/354PgrxMywXnAwEr1QZcOr6gto+ugjYE= +github.com/sagikazarmark/slog-shim v0.1.0/go.mod h1:SrcSrq8aKtyuqEI1uvTDTK1arOWRIczQRv+GVI1AkeQ= +github.com/secure-systems-lab/go-securesystemslib v0.4.0 h1:b23VGrQhTA8cN2CbBw7/FulN9fTtqYUdS5+Oxzt+DUE= +github.com/secure-systems-lab/go-securesystemslib v0.4.0/go.mod h1:FGBZgq2tXWICsxWQW1msNf49F0Pf2Op5Htayx335Qbs= +github.com/shibumi/go-pathspec v1.3.0 h1:QUyMZhFo0Md5B8zV8x2tesohbb5kfbpTi9rBnKh5dkI= +github.com/shibumi/go-pathspec v1.3.0/go.mod h1:Xutfslp817l2I1cZvgcfeMQJG5QnU2lh5tVaaMCl3jE= +github.com/shirou/gopsutil/v3 v3.24.5 h1:i0t8kL+kQTvpAYToeuiVk3TgDeKOFioZO3Ztz/iZ9pI= +github.com/shirou/gopsutil/v3 v3.24.5/go.mod h1:bsoOS1aStSs9ErQ1WWfxllSeS1K5D+U30r2NfcubMVk= +github.com/shoenig/go-m1cpu v0.1.6 h1:nxdKQNcEB6vzgA2E2bvzKIYRuNj7XNJ4S/aRSwKzFtM= +github.com/shoenig/go-m1cpu v0.1.6/go.mod h1:1JJMcUBvfNwpq05QDQVAnx3gUHr9IYF7GNg9SUEw2VQ= +github.com/shoenig/test v0.6.4 h1:kVTaSd7WLz5WZ2IaoM0RSzRsUD+m8wRR+5qvntpn4LU= +github.com/shoenig/test v0.6.4/go.mod h1:byHiCGXqrVaflBLAMq/srcZIHynQPQgeyvkvXnjqq0k= +github.com/sirupsen/logrus v1.0.6/go.mod h1:pMByvHTf9Beacp5x1UXfOR9xyW/9antXMhjMPG0dEzc= +github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= +github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMBDgk/93Q= +github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= +github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= +github.com/sourcegraph/conc v0.3.0 h1:OQTbbt6P72L20UqAkXXuLOj79LfEanQ+YQFNpLA9ySo= +github.com/sourcegraph/conc v0.3.0/go.mod h1:Sdozi7LEKbFPqYX2/J+iBAM6HpqSLTASQIKqDmF7Mt0= +github.com/spdx/tools-golang v0.5.3 h1:ialnHeEYUC4+hkm5vJm4qz2x+oEJbS0mAMFrNXdQraY= +github.com/spdx/tools-golang v0.5.3/go.mod h1:/ETOahiAo96Ob0/RAIBmFZw6XN0yTnyr/uFZm2NTMhI= +github.com/spf13/afero v1.11.0 h1:WJQKhtpdm3v2IzqG8VMqrr6Rf3UYpEF239Jy9wNepM8= +github.com/spf13/afero v1.11.0/go.mod h1:GH9Y3pIexgf1MTIWtNGyogA5MwRIDXGUr+hbWNoBjkY= +github.com/spf13/cast v0.0.0-20150508191742-4d07383ffe94/go.mod h1:r2rcYCSwa1IExKTDiTfzaxqT2FNHs8hODu4LnUfgKEg= +github.com/spf13/cast v1.6.0 h1:GEiTHELF+vaR5dhz3VqZfFSzZjYbgeKDpBxQVS4GYJ0= +github.com/spf13/cast v1.6.0/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo= +github.com/spf13/cobra v0.0.1/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ= +github.com/spf13/cobra v1.8.1 h1:e5/vxKd/rZsfSJMUX1agtjeTDf+qv1/JdBF8gg5k9ZM= +github.com/spf13/cobra v1.8.1/go.mod h1:wHxEcudfqmLYa8iTfL+OuZPbBZkmvliBWKIezN3kD9Y= +github.com/spf13/jwalterweatherman v0.0.0-20141219030609-3d60171a6431/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo= +github.com/spf13/pflag v1.0.0/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= +github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= +github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/spf13/viper v0.0.0-20150530192845-be5ff3e4840c/go.mod h1:A8kyI5cUJhb8N+3pkfONlcEcZbueH6nhAm0Fq7SrnBM= +github.com/spf13/viper v1.19.0 h1:RWq5SEjt8o25SROyN3z2OrDB9l7RPd3lwTWU8EcEdcI= +github.com/spf13/viper v1.19.0/go.mod h1:GQUN9bilAbhU/jgc1bKs99f/suXKeUMct8Adx5+Ntkg= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= +github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= +github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= +github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= +github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/studio-b12/gowebdav v0.9.0 h1:1j1sc9gQnNxbXXM4M/CebPOX4aXYtr7MojAVcN4dHjU= +github.com/studio-b12/gowebdav v0.9.0/go.mod h1:bHA7t77X/QFExdeAnDzK6vKM34kEZAcE1OX4MfiwjkE= +github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8= +github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU= +github.com/swaggo/swag v1.16.3 h1:PnCYjPCah8FK4I26l2F/KQ4yz3sILcVUN3cTlBFA9Pg= +github.com/swaggo/swag v1.16.3/go.mod h1:DImHIuOFXKpMFAQjcC7FG4m3Dg4+QuUgUzJmKjI/gRk= +github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.0.563/go.mod h1:7sCQWVkxcsR38nffDW057DRGk8mUjK1Ing/EFOK8s8Y= +github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.0.898 h1:ERwcXqhc94L9cFxtiI0pvt7IJtlHl/p/Jayl3mLw+ms= +github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.0.898/go.mod h1:r5r4xbfxSaeR04b166HGsBa/R4U3SueirEUpXGuw+Q0= +github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/dnspod v1.0.898 h1:LoYv5u+gUoFpU/AmIuTRG/2KiEkdm9gCC0dTvk8WITQ= +github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/dnspod v1.0.898/go.mod h1:c1j6YQ+vCbeA8kJ59Im4UnMd1GxovlpPBDhGZoewfn8= +github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/kms v1.0.563/go.mod h1:uom4Nvi9W+Qkom0exYiJ9VWJjXwyxtPYTkKkaLMlfE0= +github.com/tencentyun/cos-go-sdk-v5 v0.7.54 h1:FRamEhNBbSeggyYfWfzFejTLftgbICocSYFk4PKTSV4= +github.com/tencentyun/cos-go-sdk-v5 v0.7.54/go.mod h1:UN+VdbCl1hg+kKi5RXqZgaP+Boqfmk+D04GRc4XFk70= +github.com/therootcompany/xz v1.0.1 h1:CmOtsn1CbtmyYiusbfmhmkpAAETj0wBIH6kCYaX+xzw= +github.com/therootcompany/xz v1.0.1/go.mod h1:3K3UH1yCKgBneZYhuQUvJ9HPD19UEXEI0BWbMn8qNMY= +github.com/theupdateframework/notary v0.7.0 h1:QyagRZ7wlSpjT5N2qQAh/pN+DVqgekv4DzbAiAiEL3c= +github.com/theupdateframework/notary v0.7.0/go.mod h1:c9DRxcmhHmVLDay4/2fUYdISnHqbFDGRSlXPO0AhYWw= +github.com/tklauser/go-sysconf v0.3.12 h1:0QaGUFOdQaIVdPgfITYzaTegZvdCjmYO52cSFAEVmqU= +github.com/tklauser/go-sysconf v0.3.12/go.mod h1:Ho14jnntGE1fpdOqQEEaiKRpvIavV0hSfmBq8nJbHYI= +github.com/tklauser/numcpus v0.6.1 h1:ng9scYS7az0Bk4OZLvrNXNSAO2Pxr1XXRAPyjhIx+Fk= +github.com/tklauser/numcpus v0.6.1/go.mod h1:1XfjsgE2zo8GVw7POkMbHENHzVg3GzmoZ9fESEdAacY= +github.com/tonistiigi/fsutil v0.0.0-20240424095704-91a3fc46842c h1:+6wg/4ORAbnSoGDzg2Q1i3CeMcT/jjhye/ZfnBHy7/M= +github.com/tonistiigi/fsutil v0.0.0-20240424095704-91a3fc46842c/go.mod h1:vbbYqJlnswsbJqWUcJN8fKtBhnEgldDrcagTgnBVKKM= +github.com/tonistiigi/go-csvvalue v0.0.0-20240710180619-ddb21b71c0b4 h1:7I5c2Ig/5FgqkYOh/N87NzoyI9U15qUPXhDD8uCupv8= +github.com/tonistiigi/go-csvvalue v0.0.0-20240710180619-ddb21b71c0b4/go.mod h1:278M4p8WsNh3n4a1eqiFcV2FGk7wE5fwUpUom9mK9lE= +github.com/tonistiigi/units v0.0.0-20180711220420-6950e57a87ea h1:SXhTLE6pb6eld/v/cCndK0AMpt1wiVFb/YYmqB3/QG0= +github.com/tonistiigi/units v0.0.0-20180711220420-6950e57a87ea/go.mod h1:WPnis/6cRcDZSUvVmezrxJPkiO87ThFYsoUiMwWNDJk= +github.com/tonistiigi/vt100 v0.0.0-20240514184818-90bafcd6abab h1:H6aJ0yKQ0gF49Qb2z5hI1UHxSQt4JMyxebFR15KnApw= +github.com/tonistiigi/vt100 v0.0.0-20240514184818-90bafcd6abab/go.mod h1:ulncasL3N9uLrVann0m+CDlJKWsIAP34MPcOJF6VRvc= +github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI= +github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08= +github.com/uber/jaeger-client-go v2.30.0+incompatible h1:D6wyKGCecFaSRUpo8lCVbaOOb6ThwMmTEbhRwtKR97o= +github.com/uber/jaeger-client-go v2.30.0+incompatible/go.mod h1:WVhlPFC8FDjOFMMWRy2pZqQJSXxYSwNYOkTr/Z6d3Kk= +github.com/uber/jaeger-lib v2.4.1+incompatible h1:td4jdvLcExb4cBISKIpHuGoVXh+dVKhn2Um6rjCsSsg= +github.com/uber/jaeger-lib v2.4.1+incompatible/go.mod h1:ComeNDZlWwrWnDv8aPp0Ba6+uUTzImX/AauajbLI56U= +github.com/ugorji/go/codec v1.2.12 h1:9LC83zGrHhuUA9l16C9AHXAqEV/2wBQ4nkvumAE65EE= +github.com/ugorji/go/codec v1.2.12/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg= +github.com/ulikunitz/xz v0.5.6/go.mod h1:2bypXElzHzzJZwzH67Y6wb67pO62Rzfn7BSiF4ABRW8= +github.com/ulikunitz/xz v0.5.10 h1:t92gobL9l3HE202wg3rlk19F6X+JOxl9BBrCCMYEYd8= +github.com/ulikunitz/xz v0.5.10/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14= +github.com/vbatts/tar-split v0.11.5 h1:3bHCTIheBm1qFTcgh9oPu+nNBtX+XJIupG/vacinCts= +github.com/vbatts/tar-split v0.11.5/go.mod h1:yZbwRsSeGjusneWgA781EKej9HF8vme8okylkAeNKLk= +github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU= +github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb h1:zGWFAtiMcyryUHoUjUJX0/lt1H2+i2Ka2n+D3DImSNo= +github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU= +github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 h1:EzJWgHovont7NscjpAxXsDA8S8BMYve8Y5+7cuRE7R0= +github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415/go.mod h1:GwrjFmJcFw6At/Gs6z4yjiIwzuJ1/+UwLxMQDVQXShQ= +github.com/xeipuuv/gojsonschema v1.2.0 h1:LhYJRs+L4fBtjZUfuSZIKGeVu0QRy8e5Xi7D17UxZ74= +github.com/xeipuuv/gojsonschema v1.2.0/go.mod h1:anYRn/JVcOK2ZgGU+IjEV4nwlhoK5sQluxsYJ78Id3Y= +github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= +github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0= +github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0= +go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= +go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= +go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= +go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= +go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= +go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0= +go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo= +go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.49.0 h1:4Pp6oUg3+e/6M4C0A/3kJ2VYa++dsWVTtGgLVj5xtHg= +go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.49.0/go.mod h1:Mjt1i1INqiaoZOMGR1RIUJN+i3ChKoFRqzrRQhlkbs0= +go.opentelemetry.io/contrib/instrumentation/net/http/httptrace/otelhttptrace v0.46.1 h1:gbhw/u49SS3gkPWiYweQNJGm/uJN5GkI/FrosxSHT7A= +go.opentelemetry.io/contrib/instrumentation/net/http/httptrace/otelhttptrace v0.46.1/go.mod h1:GnOaBaFQ2we3b9AGWJpsBa7v1S5RlQzlC3O7dRMxZhM= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.49.0 h1:jq9TW8u3so/bN+JPT166wjOI6/vQPF6Xe7nMNIltagk= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.49.0/go.mod h1:p8pYQP+m5XfbZm9fxtSKAbM6oIllS7s2AfxrChvc7iw= +go.opentelemetry.io/otel v1.24.0 h1:0LAOdjNmQeSTzGBzduGe/rU4tZhMwL5rWgtp9Ku5Jfo= +go.opentelemetry.io/otel v1.24.0/go.mod h1:W7b9Ozg4nkF5tWI5zsXkaKKDjdVjpD4oAt9Qi/MArHo= +go.opentelemetry.io/otel/exporters/otlp/otlpmetric v0.42.0 h1:ZtfnDL+tUrs1F0Pzfwbg2d59Gru9NCH3bgSHBM6LDwU= +go.opentelemetry.io/otel/exporters/otlp/otlpmetric v0.42.0/go.mod h1:hG4Fj/y8TR/tlEDREo8tWstl9fO9gcFkn4xrx0Io8xU= +go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v0.42.0 h1:NmnYCiR0qNufkldjVvyQfZTHSdzeHoZ41zggMsdMcLM= +go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v0.42.0/go.mod h1:UVAO61+umUsHLtYb8KXXRoHtxUkdOPkYidzW3gipRLQ= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.21.0 h1:cl5P5/GIfFh4t6xyruOgJP5QiA1pw4fYYdv6nc6CBWw= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.21.0/go.mod h1:zgBdWWAu7oEEMC06MMKc5NLbA/1YDXV1sMpSqEeLQLg= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.21.0 h1:tIqheXEFWAZ7O8A7m+J0aPTmpJN3YQ7qetUAdkkkKpk= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.21.0/go.mod h1:nUeKExfxAQVbiVFn32YXpXZZHZ61Cc3s3Rn1pDBGAb0= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.21.0 h1:digkEZCJWobwBqMwC0cwCq8/wkkRy/OowZg5OArWZrM= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.21.0/go.mod h1:/OpE/y70qVkndM0TrxT4KBoN3RsFZP0QaofcfYrj76I= +go.opentelemetry.io/otel/metric v1.24.0 h1:6EhoGWWK28x1fbpA4tYTOWBkPefTDQnb8WSGXlc88kI= +go.opentelemetry.io/otel/metric v1.24.0/go.mod h1:VYhLe1rFfxuTXLgj4CBiyz+9WYBA8pNGJgDcSFRKBco= +go.opentelemetry.io/otel/sdk v1.21.0 h1:FTt8qirL1EysG6sTQRZ5TokkU8d0ugCj8htOgThZXQ8= +go.opentelemetry.io/otel/sdk v1.21.0/go.mod h1:Nna6Yv7PWTdgJHVRD9hIYywQBRx7pbox6nwBnZIxl/E= +go.opentelemetry.io/otel/sdk/metric v1.21.0 h1:smhI5oD714d6jHE6Tie36fPx4WDFIg+Y6RfAY4ICcR0= +go.opentelemetry.io/otel/sdk/metric v1.21.0/go.mod h1:FJ8RAsoPGv/wYMgBdUJXOm+6pzFY3YdljnXtv1SBE8Q= +go.opentelemetry.io/otel/trace v1.24.0 h1:CsKnnL4dUAr/0llH9FKuc698G04IrpWV0MQA/Y1YELI= +go.opentelemetry.io/otel/trace v1.24.0/go.mod h1:HPc3Xr/cOApsBI154IU0OI0HJexz+aw5uPdbs3UCjNU= +go.opentelemetry.io/proto/otlp v1.0.0 h1:T0TX0tmXU8a3CbNXzEKGeU5mIVOdf0oykP+u2lIVU/I= +go.opentelemetry.io/proto/otlp v1.0.0/go.mod h1:Sy6pihPLfYHkr3NkUbEhGHFhINUSI/v80hjKIs5JXpM= +go.uber.org/atomic v1.9.0 h1:ECmE8Bn/WFTYwEW/bpKD3M8VtR/zQVbavAoalC1PYyE= +go.uber.org/atomic v1.9.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= +go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= +go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= +go.uber.org/multierr v1.9.0 h1:7fIwc/ZtS0q++VgcfqFDxSBZVv/Xo49/SYnDFupUwlI= +go.uber.org/multierr v1.9.0/go.mod h1:X2jQV1h+kxSjClGpnseKVIxpmcjrj7MNnI0bnlfKTVQ= +go4.org v0.0.0-20200411211856-f5505b9728dd h1:BNJlw5kRTzdmyfh5U8F93HA2OwkP7ZGwA51eJ/0wKOU= +go4.org v0.0.0-20200411211856-f5505b9728dd/go.mod h1:CIiUVy99QCPfoE13bO4EZaz5GZMZXMSBGhxRdsvzbkg= +golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8= +golang.org/x/arch v0.8.0 h1:3wRIsP3pM4yUptoR96otTUOXI367OS0+c9eeRi9doIc= +golang.org/x/arch v0.8.0/go.mod h1:FEVrYAQjsQXMVJ1nsMoVVXPZg6p2JE2mx8psSWTDQys= +golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20190325154230-a5d413f7728c/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20191205180655-e7c4368fe9dd/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20200302210943-78000ba7a073/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20201117144127-c1f2f97bffc9/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I= +golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/crypto v0.1.0/go.mod h1:RecgLatLF4+eUMCP1PoPZQb+cVrJcOPbHkTkbkB9sbw= +golang.org/x/crypto v0.25.0 h1:ypSNr+bnYL2YhwoMt2zPxHFmbAN1KZs/njMG3hxUp30= +golang.org/x/crypto v0.25.0/go.mod h1:T+wALwcMOSE0kXgUAnPAHqTLW+XHgcELELW8VaDgm/M= +golang.org/x/exp v0.0.0-20180321215751-8460e604b9de/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/exp v0.0.0-20180807140117-3d87b88a115f/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/exp v0.0.0-20190125153040-c74c464bbbf2/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= +golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek= +golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY= +golang.org/x/exp v0.0.0-20191129062945-2f5052295587/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= +golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= +golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= +golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM= +golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU= +golang.org/x/exp v0.0.0-20240112132812-db7319d0e0e3 h1:hNQpMuAJe5CtcUqCXaWga3FHu+kQvCqcsoVaQgSV60o= +golang.org/x/exp v0.0.0-20240112132812-db7319d0e0e3/go.mod h1:idGWGoKP1toJGkd5/ig9ZLuPcZBC3ewk7SzmH0uou08= +golang.org/x/image v0.0.0-20180708004352-c73c2afc3b81/go.mod h1:ux5Hcp/YLpHSI86hEcLt0YII63i6oz57MZXIpbrjZUs= +golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= +golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= +golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= +golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= +golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= +golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRuDixDT3tpyyb+LUpUlRWLxfhWrs= +golang.org/x/lint v0.0.0-20200130185559-910be7a94367/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= +golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= +golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE= +golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o= +golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= +golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY= +golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= +golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= +golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= +golang.org/x/mod v0.18.0 h1:5+9lSbEzPSdWkH32vYPBwEpX8KwDbM52Ud9xBUvNlb0= +golang.org/x/mod v0.18.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= +golang.org/x/net v0.0.0-20180218175443-cbe0f9307d01/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= +golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190628185345-da137c7871d7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200222125558-5a598a2470a0/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200501053045-e0ff5e5a1de5/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200506145744-7e3656a0809f/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200513185701-a91f0712d120/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200520182314-0ba52f642ac2/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= +golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= +golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= +golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20210421230115-4e50805a0758/go.mod h1:72T/g9IO56b78aLF+1Kcs5dz7/ng1VjMUvfKvpfy+jM= +golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= +golang.org/x/net v0.1.0/go.mod h1:Cx3nUiGt4eDBEyega/BKRp+/AlGL8hYe7U9odMt2Cco= +golang.org/x/net v0.27.0 h1:5K3Njcw06/l2y9vpGCSdcxWOYHOUk3dVNGDXN+FvAys= +golang.org/x/net v0.27.0/go.mod h1:dDi0PyhWNoiUOrAS8uXv/vnScO4wnHQO4mj9fn/RytE= +golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= +golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20210313182246-cd4f82c27b84/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.21.0 h1:tsimM75w1tF/uws5rbeHzIWxEqElMehnc+iW793zsZs= +golang.org/x/oauth2 v0.21.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI= +golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M= +golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190801041406-cbf593c0f2f3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200331124033-c3d80250170d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200501052902-10377860bb8e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200511232937-7e40ca221e25/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200515095857-1151b9dac4a9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200523222454-059865788121/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201204225414-ed752295db88/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210420072515-93ed5bcd2bfe/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20211025201205-69cdffdb9359/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20221010170243-090e33056c14/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.22.0 h1:RI27ohtqKCnwULzJLqkv897zojh5/DwS/ENaMzUOaWI= +golang.org/x/sys v0.22.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/term v0.1.0/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/term v0.22.0 h1:BbsgPEJULsl2fV/AT3v15Mjva5yXKQDyKf+TbDz7QJk= +golang.org/x/term v0.22.0/go.mod h1:F3qCibpT5AMpCRfhfT53vVJwhLtIVHhB9XDjfFvnMI4= +golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= +golang.org/x/text v0.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4= +golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI= +golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk= +golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= +golang.org/x/tools v0.0.0-20180525024113-a5b4c53f6e8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190206041539-40960b6deb8e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= +golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191113191852-77e3bb0ad9e7/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191115202509-3a792d9c32b2/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191125144606-a911d9008d1f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191130070609-6e064ea0cf2d/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191216173652-a0e659d51361/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20191227053925-7b8e75db28f4/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200117161641-43d50277825c/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200122220014-bf1340f18c4a/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200204074204-1cc6d1ef6c74/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200212150539-ea181f53ac56/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200224181240-023911ca70b2/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200227222343-706bc42d1f0d/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200304193943-95d2e580d8eb/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw= +golang.org/x/tools v0.0.0-20200312045724-11d5b4c81c7d/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw= +golang.org/x/tools v0.0.0-20200331025713-a30bf2db82d4/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8= +golang.org/x/tools v0.0.0-20200501065659-ab2804fb9c9d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200512131952-2bc93b1c0c88/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200515010526-7d3b6ebf133d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200618134242-20370b0cb4b2/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200729194436-6467de6f59a7/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= +golang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= +golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= +golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= +golang.org/x/tools v0.22.0 h1:gqSGLZqv+AI9lIQzniJ0nZDRG5GBPsSi+DRNHWNz6yA= +golang.org/x/tools v0.22.0/go.mod h1:aCwcsjqvq7Yqt6TNyX7QMU2enbQ/Gt0bo6krSeEri+c= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +gonum.org/v1/gonum v0.0.0-20180816165407-929014505bf4/go.mod h1:Y+Yx5eoAFn32cQvJDxZx5Dpnq+c3wtXuadVZAcxbbBo= +gonum.org/v1/gonum v0.8.2/go.mod h1:oe/vMfY3deqTw+1EZJhuvEW2iwGF1bW9wwu7XCu0+v0= +gonum.org/v1/netlib v0.0.0-20190313105609-8cb42192e0e0/go.mod h1:wa6Ws7BG/ESfp6dHfk7C6KdzKA7wR7u/rKwOGE66zvw= +gonum.org/v1/plot v0.0.0-20190515093506-e2840ee46a6b/go.mod h1:Wt8AAjI+ypCyYX3nZBvf6cAIx93T+c/OS2HFAYskSZc= +google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE= +google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M= +google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= +google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= +google.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= +google.golang.org/api v0.14.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= +google.golang.org/api v0.15.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= +google.golang.org/api v0.17.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.18.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.19.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.20.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.22.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.24.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE= +google.golang.org/api v0.28.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE= +google.golang.org/api v0.29.0/go.mod h1:Lcubydp8VUV7KeIHD9z2Bys/sm/vGKnG1UHuDBSrHWM= +google.golang.org/api v0.30.0/go.mod h1:QGmEvQ87FHZNiUVJkT14jQNYJ4ZJjdRF23ZXz5138Fc= +google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= +google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0= +google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= +google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= +google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= +google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= +google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= +google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8= +google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20191115194625-c23dd37a84c9/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20191216164720-4f79533eabd1/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20191230161307-f3c370f40bfb/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20200115191322-ca5a22157cba/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20200122232147-0452cf42e150/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20200204135345-fa8e72b47b90/go.mod h1:GmwEX6Z4W5gMy59cAlVYjN9JhxgbQH6Gn+gFDQe2lzA= +google.golang.org/genproto v0.0.0-20200212174721-66ed5ce911ce/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200224152610-e50cd9704f63/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200228133532-8c2c7df3a383/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200305110556-506484158171/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200312145019-da6875a35672/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200331122359-1ee6d9798940/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200430143042-b979b6f78d84/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200511104702-f5ebc3bea380/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200515170657-fc4c6c6a6587/go.mod h1:YsZOwe1myG/8QRHRsmBRE1LrgQY60beZKjly0O1fX9U= +google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= +google.golang.org/genproto v0.0.0-20200618031413-b414f8b61790/go.mod h1:jDfRM7FcilCzHH/e9qn6dsT145K34l5v+OpcnNgKAAA= +google.golang.org/genproto v0.0.0-20200729003335-053ba62fc06f/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20200804131852-c06518451d9c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20200825200019-8632dd797987/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20240227224415-6ceb2ff114de h1:F6qOa9AZTYJXOUEr4jDysRDLrm4PHePlge4v4TGAlxY= +google.golang.org/genproto v0.0.0-20240227224415-6ceb2ff114de/go.mod h1:VUhTRKeHn9wwcdrk73nvdC9gF178Tzhmt/qyaFcPLSo= +google.golang.org/genproto/googleapis/api v0.0.0-20240311132316-a219d84964c2 h1:rIo7ocm2roD9DcFIX67Ym8icoGCKSARAiPljFhh5suQ= +google.golang.org/genproto/googleapis/api v0.0.0-20240311132316-a219d84964c2/go.mod h1:O1cOfN1Cy6QEYr7VxtjOyP5AdAuR0aJ/MYZaaof623Y= +google.golang.org/genproto/googleapis/rpc v0.0.0-20240318140521-94a12d6c2237 h1:NnYq6UN9ReLM9/Y01KWNOWyI5xQ9kbIms5GGJVwS/Yc= +google.golang.org/genproto/googleapis/rpc v0.0.0-20240318140521-94a12d6c2237/go.mod h1:WtryC6hu0hhx87FDGxWCDptyssuo68sk10vYjF+T9fY= +google.golang.org/grpc v1.0.5/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmEdcZw= +google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= +google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= +google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= +google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= +google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= +google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +google.golang.org/grpc v1.27.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +google.golang.org/grpc v1.28.0/go.mod h1:rpkK4SK4GF4Ach/+MFLZUBavHOvF2JJB5uozKKal+60= +google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk= +google.golang.org/grpc v1.30.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= +google.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= +google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc= +google.golang.org/grpc v1.63.1 h1:pNClQmvdlyNUiwFETOux/PYqfhmA7BrswEdGRnib1fA= +google.golang.org/grpc v1.63.1/go.mod h1:WAX/8DgncnokcFUldAxq7GeB5DXHDbMF+lLvDomNkRA= +google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= +google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= +google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= +google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= +google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= +google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4= +google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= +google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= +google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= +google.golang.org/protobuf v1.34.1 h1:9ddQBjfCyZPOHPUiPxpYESBLc+T8P3E+Vo4IbKZgFWg= +google.golang.org/protobuf v1.34.1/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= +gopkg.in/airbrake/gobrake.v2 v2.0.9/go.mod h1:/h5ZAUhDkGaJfjzjKLSjv6zCL6O0LLBxU4K+aSYdM/U= +gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= +gopkg.in/cenkalti/backoff.v2 v2.2.1 h1:eJ9UAg01/HIHG987TwxvnzK2MgxXq97YY6rYDpY9aII= +gopkg.in/cenkalti/backoff.v2 v2.2.1/go.mod h1:S0QdOvT2AlerfSBkp0O+dk+bbIMaNbEmVk876gPCthU= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= +gopkg.in/fsnotify.v1 v1.4.7 h1:xOHLXZwVvI9hhs+cLKq5+I5onOuwQLhQwiu63xxlHs4= +gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= +gopkg.in/gemnasium/logrus-airbrake-hook.v2 v2.1.2/go.mod h1:Xk6kEKp8OKb+X14hQBKWaSkCsqBpgog8nAV2xsGOxlo= +gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc= +gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= +gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA= +gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= +gopkg.in/rethinkdb/rethinkdb-go.v6 v6.2.1 h1:d4KQkxAaAiRY2h5Zqis161Pv91A37uZyJOx73duwUwM= +gopkg.in/rethinkdb/rethinkdb-go.v6 v6.2.1/go.mod h1:WbjuEoo1oadwzQ4apSDU+JTvmllEHtsNHS6y7vFc7iw= +gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= +gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= +gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.0/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gorm.io/gorm v1.25.11 h1:/Wfyg1B/je1hnDx3sMkX+gAlxrlZpn6X0BXRlwXlvHg= +gorm.io/gorm v1.25.11/go.mod h1:xh7N7RHfYlNc5EmcI/El95gXusucDrQnHXe0+CgWcLQ= +gotest.tools/v3 v3.5.1 h1:EENdUnS3pdur5nybKYIh2Vfgc8IUNBjxDPSjtiJcOzU= +gotest.tools/v3 v3.5.1/go.mod h1:isy3WKz7GK6uNw/sbHzfKBLvlvXwUyV06n6brMxxopU= +honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= +honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= +honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= +k8s.io/api v0.29.2 h1:hBC7B9+MU+ptchxEqTNW2DkUosJpp1P+Wn6YncZ474A= +k8s.io/api v0.29.2/go.mod h1:sdIaaKuU7P44aoyyLlikSLayT6Vb7bvJNCX105xZXY0= +k8s.io/apimachinery v0.29.2 h1:EWGpfJ856oj11C52NRCHuU7rFDwxev48z+6DSlGNsV8= +k8s.io/apimachinery v0.29.2/go.mod h1:6HVkd1FwxIagpYrHSwJlQqZI3G9LfYWRPAkUvLnXTKU= +k8s.io/client-go v0.29.2 h1:FEg85el1TeZp+/vYJM7hkDlSTFZ+c5nnK44DJ4FyoRg= +k8s.io/client-go v0.29.2/go.mod h1:knlvFZE58VpqbQpJNbCbctTVXcd35mMyAAwBdpt4jrA= +k8s.io/klog/v2 v2.110.1 h1:U/Af64HJf7FcwMcXyKm2RPM22WZzyR7OSpYj5tg3cL0= +k8s.io/klog/v2 v2.110.1/go.mod h1:YGtd1984u+GgbuZ7e08/yBuAfKLSO0+uR1Fhi6ExXjo= +k8s.io/kube-openapi v0.0.0-20231010175941-2dd684a91f00 h1:aVUu9fTY98ivBPKR9Y5w/AuzbMm96cd3YHRTU83I780= +k8s.io/kube-openapi v0.0.0-20231010175941-2dd684a91f00/go.mod h1:AsvuZPBlUDVuCdzJ87iajxtXuR9oktsTctW/R9wwouA= +k8s.io/utils v0.0.0-20230726121419-3b25d923346b h1:sgn3ZU783SCgtaSJjpcVVlRqd6GSnlTLKgpAAttJvpI= +k8s.io/utils v0.0.0-20230726121419-3b25d923346b/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= +modernc.org/libc v1.22.5 h1:91BNch/e5B0uPbJFgqbxXuOnxBQjlS//icfQEGmvyjE= +modernc.org/libc v1.22.5/go.mod h1:jj+Z7dTNX8fBScMVNRAYZ/jF91K8fdT2hYMThc3YjBY= +modernc.org/mathutil v1.5.0 h1:rV0Ko/6SfM+8G+yKiyI830l3Wuz1zRutdslNoQ0kfiQ= +modernc.org/mathutil v1.5.0/go.mod h1:mZW8CKdRPY1v87qxC/wUdX5O1qDzXMP5TH3wjfpga6E= +modernc.org/memory v1.5.0 h1:N+/8c5rE6EqugZwHii4IFsaJ7MUhoWX07J5tC/iI5Ds= +modernc.org/memory v1.5.0/go.mod h1:PkUhL0Mugw21sHPeskwZW4D6VscE/GQJOnIpCnW6pSU= +modernc.org/sqlite v1.23.1 h1:nrSBg4aRQQwq59JpvGEQ15tNxoO5pX/kUjcRNwSAGQM= +modernc.org/sqlite v1.23.1/go.mod h1:OrDj17Mggn6MhE+iPbBNf7RGKODDE9NFT0f3EwDzJqk= +nullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYmp6pbG50= +rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= +rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4= +rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0= +rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA= +sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd h1:EDPBXCAspyGV4jQlpZSudPeMmr1bNJefnuqLsRAsHZo= +sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd/go.mod h1:B8JuhiUyNFVKdsE8h686QcCxMaH6HrOAZj4vswFpcB0= +sigs.k8s.io/structured-merge-diff/v4 v4.4.1 h1:150L+0vs/8DA78h1u02ooW1/fFq/Lwr+sGiqlzvrtq4= +sigs.k8s.io/structured-merge-diff/v4 v4.4.1/go.mod h1:N8hJocpFajUSSeSJ9bOZ77VzejKZaXsTtZo4/u7Io08= +sigs.k8s.io/yaml v1.3.0 h1:a2VclLzOGrwOHDiV8EfBGhvjHvP46CtW5j6POvhYGGo= +sigs.k8s.io/yaml v1.3.0/go.mod h1:GeOyir5tyXNByN85N/dRIT9es5UQNerPYEKK56eTBm8= diff --git a/agent/i18n/i18n.go b/agent/i18n/i18n.go new file mode 100644 index 000000000..a8a834e45 --- /dev/null +++ b/agent/i18n/i18n.go @@ -0,0 +1,131 @@ +package i18n + +import ( + "embed" + "strings" + + "github.com/1Panel-dev/1Panel/agent/global" + + "github.com/gin-gonic/gin" + "github.com/nicksnyder/go-i18n/v2/i18n" + "golang.org/x/text/language" + "gopkg.in/yaml.v3" +) + +func GetMsgWithMap(key string, maps map[string]interface{}) string { + var content string + if maps == nil { + content, _ = global.I18n.Localize(&i18n.LocalizeConfig{ + MessageID: key, + }) + } else { + content, _ = global.I18n.Localize(&i18n.LocalizeConfig{ + MessageID: key, + TemplateData: maps, + }) + } + content = strings.ReplaceAll(content, ": ", "") + if content == "" { + return key + } else { + return content + } +} + +func GetMsgWithName(key string, name string, err error) string { + var ( + content string + dataMap = make(map[string]interface{}) + ) + dataMap["name"] = name + if err != nil { + dataMap["err"] = err.Error() + } + content, _ = global.I18n.Localize(&i18n.LocalizeConfig{ + MessageID: key, + TemplateData: dataMap, + }) + content = strings.ReplaceAll(content, "", "") + if content == "" { + return key + } else { + return content + } +} + +func GetErrMsg(key string, maps map[string]interface{}) string { + var content string + if maps == nil { + content, _ = global.I18n.Localize(&i18n.LocalizeConfig{ + MessageID: key, + }) + } else { + content, _ = global.I18n.Localize(&i18n.LocalizeConfig{ + MessageID: key, + TemplateData: maps, + }) + } + return content +} + +func GetMsgByKey(key string) string { + content, _ := global.I18n.Localize(&i18n.LocalizeConfig{ + MessageID: key, + }) + return content +} + +func GetWithName(key string, name string) string { + var ( + dataMap = make(map[string]interface{}) + ) + dataMap["name"] = name + content, _ := global.I18n.Localize(&i18n.LocalizeConfig{ + MessageID: key, + TemplateData: dataMap, + }) + return content +} + +func GetWithMap(key string, dataMap map[string]string) string { + content, _ := global.I18n.Localize(&i18n.LocalizeConfig{ + MessageID: key, + TemplateData: dataMap, + }) + return content +} + +func GetWithNameAndErr(key string, name string, err error) string { + var ( + dataMap = make(map[string]interface{}) + ) + dataMap["name"] = name + dataMap["err"] = err.Error() + content, _ := global.I18n.Localize(&i18n.LocalizeConfig{ + MessageID: key, + TemplateData: dataMap, + }) + return content +} + +//go:embed lang/* +var fs embed.FS +var bundle *i18n.Bundle + +func UseI18n() gin.HandlerFunc { + return func(context *gin.Context) { + lang := context.GetHeader("Accept-Language") + if lang == "" { + lang = "zh" + } + global.I18n = i18n.NewLocalizer(bundle, lang) + } +} + +func Init() { + bundle = i18n.NewBundle(language.Chinese) + bundle.RegisterUnmarshalFunc("yaml", yaml.Unmarshal) + _, _ = bundle.LoadMessageFileFS(fs, "lang/zh.yaml") + _, _ = bundle.LoadMessageFileFS(fs, "lang/en.yaml") + _, _ = bundle.LoadMessageFileFS(fs, "lang/zh-Hant.yaml") +} diff --git a/agent/i18n/lang/en.yaml b/agent/i18n/lang/en.yaml new file mode 100644 index 000000000..69b99e474 --- /dev/null +++ b/agent/i18n/lang/en.yaml @@ -0,0 +1,207 @@ +ErrInvalidParams: "Request parameter error: {{ .detail }}" +ErrTokenParse: "Token generation error: {{ .detail }}" +ErrInitialPassword: "Initial password error" +ErrInternalServer: "Service internal error: {{ .detail }}" +ErrRecordExist: "Record already exists" +ErrRecordNotFound: "Records not found" +ErrStructTransform: "Type conversion failure: {{ .detail }}" +ErrNotLogin: "User is not Login: {{ .detail }}" +ErrPasswordExpired: "The current password has expired: {{ .detail }}" +ErrNotSupportType: "The system does not support the current type: {{ .detail }}" + +#common +ErrNameIsExist: "Name is already exist" +ErrDemoEnvironment: "Demo server, prohibit this operation!" +ErrCmdTimeout: "Command execution timed out!" +ErrCmdIllegal: "The command contains illegal characters. Please modify and try again!" +ErrPortExist: '{{ .port }} port is already occupied by {{ .type }} [{{ .name }}]' +TYPE_APP: "Application" +TYPE_RUNTIME: "Runtime environment" +TYPE_DOMAIN: "Domain name" +ErrTypePort: 'Port {{ .name }} format error' +ErrTypePortRange: 'Port range needs to be between 1-65535' +Success: "Success" +Failed: "Failed" +SystemRestart: "System restart causes task interruption" + +#app +ErrPortInUsed: "{{ .detail }} port already in use" +ErrAppLimit: "App exceeds install limit" +ErrAppRequired: "{{ .detail }} app is required" +ErrNotInstall: "App not installed" +ErrPortInOtherApp: "{{ .port }} port already in use by app {{ .apps }}" +ErrDbUserNotValid: "Stock database, username and password do not match!" +ErrDockerComposeNotValid: "docker-compose file format error!" +ErrUpdateBuWebsite: 'The application was updated successfully, but the modification of the website configuration file failed, please check the configuration!' +Err1PanelNetworkFailed: 'Default container network creation failed! {{ .detail }}' +ErrFileParse: 'Application docker-compose file parsing failed!' +ErrInstallDirNotFound: 'installation directory does not exist' +AppStoreIsUpToDate: 'The app store is already up to date!' +LocalAppVersionNull: 'The {{.name}} app is not synced to version! Could not add to application list' +LocalAppVersionErr: '{{.name}} failed to sync version {{.version}}! {{.err}}' +ErrFileNotFound: '{{.name}} file does not exist' +ErrFileParseApp: 'Failed to parse {{.name}} file {{.err}}' +ErrAppDirNull: 'version folder does not exist' +LocalAppErr: "App {{.name}} sync failed! {{.err}}" +ErrContainerName: "ContainerName is already exist" +ErrAppSystemRestart: "1Panel restart causes the task to terminate" +ErrCreateHttpClient: "Failed to create HTTP request {{.err}}" +ErrHttpReqTimeOut: "Request timed out {{.err}}" +ErrHttpReqFailed: "Request failed {{.err}}" +ErrHttpReqNotFound: "The file does not exist" +ErrNoSuchHost: "Network connection failed" +ErrImagePullTimeOut: 'Image pull timeout' +ErrContainerNotFound: '{{ .name }} container does not exist' +ErrContainerMsg: '{{ .name }} container is abnormal, please check the log on the container page for details' +ErrAppBackup: '{{ .name }} application backup failed err {{.err}}' +ErrImagePull: '{{ .name }} image pull failed err {{.err}}' +ErrVersionTooLow: 'The current 1Panel version is too low to update the app store, please upgrade the version' +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" +ErrAppUpgrade: "Failed to upgrade application {{ .name }} {{ .err }}" +AppRecover: "App {{ .name }} rolled back " +PullImageStart: "Start pulling image {{ .name }}" +PullImageSuccess: "Image pulled successfully" +UpgradeAppStart: "Start upgrading application {{ .name }}" +UpgradeAppSuccess: "App {{ .name }} upgraded successfully" + +#file +ErrFileCanNotRead: "File can not read" +ErrFileToLarge: "file is too large" +ErrPathNotFound: "Path is not found" +ErrMovePathFailed: "The target path cannot contain the original path!" +ErrLinkPathNotFound: "Target path does not exist!" +ErrFileIsExist: "File or directory already exists!" +ErrFileUpload: "Failed to upload file {{.name}} {{.detail}}" +ErrFileDownloadDir: "Download folder not supported" +ErrCmdNotFound: "{{ .name}} command does not exist, please install this command on the host first" +ErrSourcePathNotFound: "Source directory does not exist" +ErrFavoriteExist: "This path has been collected" +ErrInvalidChar: "Illegal characters are prohibited" + +#website +ErrDomainIsExist: "Domain is already exist" +ErrAliasIsExist: "Alias is already exist" +ErrAppDelete: 'Other Website use this App' +ErrGroupIsUsed: 'The group is in use and cannot be deleted' +ErrBackupMatch: 'the backup file does not match the current partial data of the website: {{ .detail}}' +ErrBackupExist: 'the backup file corresponds to a portion of the original data that does not exist: {{ .detail}}' +ErrPHPResource: 'The local runtime does not support switching!' +ErrPathPermission: 'A folder with non-1000:1000 permissions was detected in the index directory, which may cause an Access denied error when accessing the website. Please click the save button above' +ErrDomainIsUsed: "Domain is already used by website {{ .name }}" +ErrDomainFormat: "{{ .name }} domain format error" +ErrDefaultAlias: "default is a reserved code name, please use another code name" + +#ssl +ErrSSLCannotDelete: "The certificate {{ .name }} is being used by the website and cannot be removed" +ErrAccountCannotDelete: "The certificate associated with the account cannot be deleted" +ErrSSLApply: "The certificate continues to be signed successfully, but openresty reload fails, please check the configuration!" +ErrEmailIsExist: 'Email is already exist' +ErrSSLKeyNotFound: 'The private key file does not exist' +ErrSSLCertificateNotFound: 'The certificate file does not exist' +ErrSSLKeyFormat: 'Private key file verification error' +ErrSSLCertificateFormat: 'Certificate file format error, please use pem format' +ErrEabKidOrEabHmacKeyCannotBlank: 'EabKid or EabHmacKey cannot be empty' +ErrOpenrestyNotFound: 'Http mode requires Openresty to be installed first' +ApplySSLStart: 'Start applying for certificate, domain name [{{ .domain }}] application method [{{ .type }}] ' +dnsAccount: "DNS Automatic" +dnsManual: "DNS Manual" +http: "HTTP" +ApplySSLFailed: 'Application for [{{ .domain }}] certificate failed, {{.detail}} ' +ApplySSLSuccess: 'Application for [{{ .domain }}] certificate successful! ! ' +DNSAccountName: 'DNS account [{{ .name }}] manufacturer [{{.type}}]' +PushDirLog: 'Certificate pushed to directory [{{ .path }}] {{ .status }}' +ErrDeleteCAWithSSL: "There is an issued certificate under the current organization and cannot be deleted" +ErrDeleteWithPanelSSL: "Panel SSL configuration uses this certificate and cannot be deleted" +ErrDefaultCA: "The default organization cannot be deleted" +ApplyWebSiteSSLLog: "Start updating {{ .name }} website certificate" +ErrUpdateWebsiteSSL: "{{ .name }} website failed to update certificate: {{ .err }}" +ApplyWebSiteSSLSuccess: "Update website certificate successfully" + +#mysql +ErrUserIsExist: "The current user already exists. Please enter a new user" +ErrDatabaseIsExist: "The current database already exists. Please enter a new database" +ErrExecTimeOut: "SQL execution timed out, please check the database" +ErrRemoteExist: "The remote database already exists with that name, please modify it and try again" +ErrLocalExist: "The local database already exists with that name, please modify it and try again" + +#redis +ErrTypeOfRedis: "The recovery file type does not match the current persistence mode. Modify the file type and try again" + +#container +ErrInUsed: "{{ .detail }} is in use and cannot be deleted" +ErrObjectInUsed: "This object is in use and cannot be deleted" +ErrPortRules: "The number of ports does not match, please re-enter!" +ErrPgImagePull: "Image pull timeout. Please configure image acceleration or manually pull the postgres:16.0-alpine image and try again" + +#runtime +ErrDirNotFound: "The build folder does not exist! Please check file integrity!" +ErrFileNotExist: "{{ .detail }} file does not exist! Please check source file integrity!" +ErrImageBuildErr: "Image build failed" +ErrImageExist: "Image is already exist!" +ErrDelWithWebsite: "The operating environment has been associated with a website and cannot be deleted" +ErrRuntimeStart: "Failed to start" +ErrPackageJsonNotFound: "package.json file does not exist" +ErrScriptsNotFound: "No scripts configuration item was found in package.json" +ErrContainerNameNotFound: "Unable to get container name, please check .env file" +ErrNodeModulesNotFound: "The node_modules folder does not exist! Please edit the running environment or wait for the running environment to start successfully" + +#setting +ErrBackupInUsed: "The backup account is already being used in a cronjob and cannot be deleted." +ErrBackupCheck: "Backup account test connection failed {{ .err}}" +ErrOSSConn: "Unable to retrieve the latest version, please check if the server can connect to the external network." +ErrEntrance: "Security entrance information error. Please check and try again!" + +#tool +ErrConfigNotFound: "Configuration file does not exist" +ErrConfigParse: "Configuration file format error" +ErrConfigIsNull: "The configuration file is not allowed to be empty" +ErrConfigDirNotFound: "The running directory does not exist" +ErrConfigAlreadyExist: "A configuration file with the same name already exists" +ErrUserFindErr: "Failed to find user {{ .name }} {{ .err }}" + +#ssh +ErrFirewall: "No firewalld or ufw service is detected. Please check and try again!" + +#cronjob +ErrBashExecute: "Script execution error, please check the specific information in the task output text area." +ErrCutWebsiteLog: "{{ .name }} website log cutting failed, error {{ .err }}" +CutWebsiteLogSuccess: "{{ .name }} website log cut successfully, backup path {{ .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!" +ErrClamdscanNotFound: "The clamdscan command was not detected, please refer to the documentation to install it!" + +#waf +ErrScope: "Modification of this configuration is not supported" +ErrStateChange: "State modification failed" +ErrRuleExist: "Rule is Exist" +ErrRuleNotExist: "Rule is not Exist" +ErrParseIP: "IP format error" +ErrDefaultIP: "default is a reserved name, please change it to another name" +ErrGroupInUse: "The IP group is used by the black/white list and cannot be deleted" +ErrGroupExist: "IP group name already exists" +ErrIPRange: "Wrong IP range" +ErrIPExist: "IP is exit" + +#license +ErrLicense: "License format error, please check and try again!" +ErrLicenseCheck: "License verification failed, please check and try again!" +ErrLicenseSave: "Failed to save license information, error {{ .err }}, please try again!" +ErrLicenseSync: "Failed to sync license information, no license information detected in the database!" +ErrXpackNotFound: "This section is a professional edition feature, please import the license first in Panel Settings-License interface" +ErrXpackNotActive: "This section is a professional edition feature, please synchronize the license status first in Panel Settings-License interface" +ErrXpackOutOfDate: "The current license has expired, please re-import the license in Panel Settings-License interface" + +#task +TaskStart: "{{.name}} started [START]" +TaskEnd: "{{.name}} ended [COMPLETED]" +TaskFailed: "{{.name}} failed: {{.err}}" +TaskTimeout: "{{.name}} timed out" +TaskSuccess: "{{.name}} succeeded" +TaskRetry: "Start {{.name}} retry" +SubTaskStart: "Start {{.name}}" diff --git a/agent/i18n/lang/zh-Hant.yaml b/agent/i18n/lang/zh-Hant.yaml new file mode 100644 index 000000000..817f38e23 --- /dev/null +++ b/agent/i18n/lang/zh-Hant.yaml @@ -0,0 +1,209 @@ +ErrInvalidParams: "請求參數錯誤: {{ .detail }}" +ErrTokenParse: "Token 產生錯誤: {{ .detail }}" +ErrInitialPassword: "原密碼錯誤" +ErrInternalServer: "伺服器內部錯誤: {{ .detail }}" +ErrRecordExist: "記錄已存在" +ErrRecordNotFound: "記錄未找到" +ErrStructTransform: "類型轉換失敗: {{ .detail }}" +ErrNotLogin: "用戶未登入: {{ .detail }}" +ErrPasswordExpired: "當前密碼已過期: {{ .detail }}" +ErrNotSupportType: "系統暫不支持當前類型: {{ .detail }}" + +#common +ErrNameIsExist: "名稱已存在" +ErrDemoEnvironment: "演示伺服器,禁止此操作!" +ErrCmdTimeout: "指令執行超時!" +ErrCmdIllegal: "執行命令中存在不合法字符,請修改後重試!" +ErrPortExist: '{{ .port }} 埠已被 {{ .type }} [{{ .name }}] 佔用' +TYPE_APP: "應用" +TYPE_RUNTIME: "運作環境" +TYPE_DOMAIN: "網域名稱" +ErrTypePort: '埠 {{ .name }} 格式錯誤' +ErrTypePortRange: '連接埠範圍需要在 1-65535 之間' +Success: "成功" +Failed: "失敗" +SystemRestart: "系統重啟導致任務中斷" +ErrInvalidChar: "禁止使用非法字元" + +#app +ErrPortInUsed: "{{ .detail }} 端口已被佔用!" +ErrAppLimit: "應用超出安裝數量限制" +ErrAppRequired: "請先安裝 {{ .detail }} 應用" +ErrNotInstall: "應用未安裝" +ErrPortInOtherApp: "{{ .port }} 端口已被應用 {{ .apps }} 佔用!" +ErrDbUserNotValid: "儲存資料庫,用戶名密碼不匹配!" +ErrDockerComposeNotValid: "docker-compose 文件格式錯誤" +ErrUpdateBuWebsite: '應用更新成功,但是網站配置文件修改失敗,請檢查配置!' +Err1PanelNetworkFailed: '默認容器網絡創建失敗!{{ .detail }}' +ErrFileParse: '應用 docker-compose 文件解析失敗!' +ErrInstallDirNotFound: '安裝目錄不存在' +AppStoreIsUpToDate: '應用商店已經是最新版本' +LocalAppVersionNull: '{{.name}} 應用未同步到版本!無法添加到應用列表' +LocalAppVersionErr: '{{.name}} 同步版本 {{.version}} 失敗!{{.err}}' +ErrFileNotFound: '{{.name}} 文件不存在' +ErrFileParseApp: '{{.name}} 文件解析失敗 {{.err}}' +ErrAppDirNull: '版本資料夾不存在' +LocalAppErr: "應用 {{.name}} 同步失敗!{{.err}}" +ErrContainerName: "容器名稱已存在" +ErrAppSystemRestart: "1Panel 重啟導致任務中斷" +ErrCreateHttpClient: "創建HTTP請求失敗 {{.err}}" +ErrHttpReqTimeOut: "請求超時 {{.err}}" +ErrHttpReqFailed: "請求失敗 {{.err}}" +ErrHttpReqNotFound: "文件不存在" +ErrNoSuchHost: "網路連接失敗" +ErrImagePullTimeOut: "鏡像拉取超時" +ErrContainerNotFound: '{{ .name }} 容器不存在' +ErrContainerMsg: '{{ .name }} 容器異常,具體請在容器頁面查看日誌' +ErrAppBackup: '{{ .name }} 應用備份失敗 err {{.err}}' +ErrImagePull: '{{ .name }} 鏡像拉取失敗 err {{.err}}' +ErrVersionTooLow: '當前 1Panel 版本過低,無法更新應用商店,請升級版本之後操作' +ErrAppNameExist: '應用名稱已存在' +AppStoreIsSyncing: '應用程式商店正在同步中,請稍後再試' +ErrGetCompose: "docker-compose.yml 檔案取得失敗!{{ .detail }}" +ErrAppWarn: "狀態異常,請查看日誌" +ErrAppParamKey: "參數 {{ .name }} 欄位異常" +ErrAppUpgrade: "應用程式 {{ .name }} 升級失敗 {{ .err }}" +AppRecover: "應用程式 {{ .name }} 回滾 " +PullImageStart: "開始拉取鏡像 {{ .name }}" +PullImageSuccess: "鏡像拉取成功" +UpgradeAppStart: "開始升級應用程式 {{ .name }}" +UpgradeAppSuccess: "應用程式 {{ .name }} 升級成功" + +#file +ErrFileCanNotRead: "此文件不支持預覽" +ErrFileToLarge: "文件超過10M,無法打開" +ErrPathNotFound: "目錄不存在" +ErrMovePathFailed: "目標路徑不能包含原路徑!" +ErrLinkPathNotFound: "目標路徑不存在!" +ErrFileIsExist: "文件或文件夾已存在!" +ErrFileUpload: "{{ .name }} 上傳文件失敗 {{ .detail}}" +ErrFileDownloadDir: "不支持下載文件夾" +ErrCmdNotFound: "{{ .name}} 命令不存在,請先在宿主機安裝此命令" +ErrSourcePathNotFound: "源目錄不存在" +ErrFavoriteExist: "已收藏此路徑" + +#website +ErrDomainIsExist: "域名已存在" +ErrAliasIsExist: "代號已存在" +ErrAppDelete: '其他網站使用此應用,無法刪除' +ErrGroupIsUsed: '分組正在使用中,無法刪除' +ErrBackupMatch: '該備份文件與當前網站部分數據不匹配: {{ .detail}}' +ErrBackupExist: '該備份文件對應部分原數據不存在: {{ .detail}}' +ErrPHPResource: '本地運行環境不支持切換!' +ErrPathPermission: 'index 目錄下偵測到非 1000:1000 權限資料夾,可能導致網站存取 Access denied 錯誤,請點擊上方儲存按鈕' +ErrDomainIsUsed: "域名已被網站【{{ .name }}】使用" +ErrDomainFormat: "{{ .name }} 域名格式不正確" +ErrDefaultAlias: "default 為保留代號,請使用其他代號" + +#ssl +ErrSSLCannotDelete: "{{ .name }} 證書正在被網站使用,無法刪除" +ErrAccountCannotDelete: "帳號關聯證書,無法刪除" +ErrSSLApply: "證書續簽成功,openresty reload失敗,請檢查配置!" +ErrEmailIsExist: '郵箱已存在' +ErrSSLKeyNotFound: '私鑰文件不存在' +ErrSSLCertificateNotFound: '證書文件不存在' +ErrSSLKeyFormat: '私鑰文件校驗錯誤' +ErrSSLCertificateFormat: '證書文件格式錯誤,請使用 pem 格式' +ErrEabKidOrEabHmacKeyCannotBlank: 'EabKid 或 EabHmacKey 不能為空' +ErrOpenrestyNotFound: 'Http 模式需要先安裝 Openresty' +ApplySSLStart: '開始申請憑證,網域 [{{ .domain }}] 申請方式 [{{ .type }}] ' +dnsAccount: "DNS 自動" +dnsManual: "DNS 手排" +http: "HTTP" +ApplySSLFailed: '申請 [{{ .domain }}] 憑證失敗, {{.detail}} ' +ApplySSLSuccess: '申請 [{{ .domain }}] 憑證成功! ! ' +DNSAccountName: 'DNS 帳號 [{{ .name }}] 廠商 [{{.type}}]' +PushDirLog: '憑證推送到目錄 [{{ .path }}] {{ .status }}' +ErrDeleteCAWithSSL: "目前機構下存在已簽發證書,無法刪除" +ErrDeleteWithPanelSSL: "面板 SSL 配置使用此證書,無法刪除" +ErrDefaultCA: "默認機構不能刪除" +ApplyWebSiteSSLLog: "開始更新 {{ .name }} 網站憑證" +ErrUpdateWebsiteSSL: "{{ .name }} 網站更新憑證失敗: {{ .err }}" +ApplyWebSiteSSLSuccess: "更新網站憑證成功" + + +#mysql +ErrUserIsExist: "當前用戶已存在,請重新輸入" +ErrDatabaseIsExist: "當前資料庫已存在,請重新輸入" +ErrExecTimeOut: "SQL 執行超時,請檢查數據庫" +ErrRemoteExist: "遠程數據庫已存在該名稱,請修改後重試" +ErrLocalExist: "本地數據庫已存在該名稱,請修改後重試" + +#redis +ErrTypeOfRedis: "恢復文件類型與當前持久化方式不符,請修改後重試" + +#container +ErrInUsed: "{{ .detail }} 正被使用,無法刪除" +ErrObjectInUsed: "該對象正被使用,無法刪除" +ErrPortRules: "端口數目不匹配,請重新輸入!" +ErrPgImagePull: "鏡像拉取超時,請配置鏡像加速或手動拉取 postgres:16.0-alpine 鏡像後重試" + +#runtime +ErrDirNotFound: "build 文件夾不存在!請檢查文件完整性!" +ErrFileNotExist: "{{ .detail }} 文件不存在!請檢查源文件完整性!" +ErrImageBuildErr: "鏡像 build 失敗" +ErrImageExist: "鏡像已存在!" +ErrDelWithWebsite: "運行環境已經關聯網站,無法刪除" +ErrRuntimeStart: "啟動失敗" +ErrPackageJsonNotFound: "package.json 文件不存在" +ErrScriptsNotFound: "沒有在 package.json 中找到 scripts 配置項" +ErrContainerNameNotFound: "無法取得容器名稱,請檢查 .env 文件" +ErrNodeModulesNotFound: "node_modules 文件夾不存在!請編輯運行環境或者等待運行環境啟動成功" + +#setting +ErrBackupInUsed: "該備份帳號已在計劃任務中使用,無法刪除" +ErrBackupCheck: "備份帳號測試連接失敗 {{ .err}}" +ErrOSSConn: "無法獲取最新版本,請確認伺服器是否能夠連接外部網路。" +ErrEntrance: "安全入口信息錯誤,請檢查後重試!" + +#tool +ErrConfigNotFound: "配置文件不存在" +ErrConfigParse: "配置文件格式有誤" +ErrConfigIsNull: "配置文件不允許為空" +ErrConfigDirNotFound: "運行目錄不存在" +ErrConfigAlreadyExist: "已存在同名配置文件" +ErrUserFindErr: "用戶 {{ .name }} 查找失敗 {{ .err }}" + +#ssh +ErrFirewall: "當前未檢測到系統 firewalld 或 ufw 服務,請檢查後重試!" + +#cronjob +ErrBashExecute: "腳本執行錯誤,請在任務輸出文本域中查看具體信息。" +ErrCutWebsiteLog: "{{ .name }} 網站日誌切割失敗,錯誤 {{ .err }}" +CutWebsiteLogSuccess: "{{ .name }} 網站日誌切割成功,備份路徑 {{ .path }}" + +#toolbox +ErrNotExistUser: "當前使用者不存在,請修改後重試!" +ErrBanAction: "設置失敗,當前 {{ .name }} 服務不可用,請檢查後重試!" +ErrClamdscanNotFound: "未偵測到 clamdscan 指令,請參考文件安裝!" + +#waf +ErrScope: "不支援修改此配置" +ErrStateChange: "狀態修改失敗" +ErrRuleExist: "規則已存在" +ErrRuleNotExist: "規則不存在" +ErrParseIP: "IP 格式錯誤" +ErrDefaultIP: "default 為保留名稱,請更換其他名稱" +ErrGroupInUse: "IP 群組被黑/白名單使用,無法刪除" +ErrGroupExist: "IP 群組名稱已存在" +ErrIPRange: "IP 範圍錯誤" +ErrIPExist: "IP 已存在" + + +#license +ErrLicense: "許可證格式錯誤,請檢查後重試!" +ErrLicenseCheck: "許可證校驗失敗,請檢查後重試!" +ErrLicenseSave: "許可證信息保存失敗,錯誤 {{ .err }}, 請重試!" +ErrLicenseSync: "許可證信息同步失敗,資料庫中未檢測到許可證信息!" +ErrXpackNotFound: "該部分為專業版功能,請先在 面板設置-許可證 界面導入許可證" +ErrXpackNotActive: "該部分為專業版功能,請先在 面板設置-許可證 界面同步許可證狀態" +ErrXpackOutOfDate: "當前許可證已過期,請重新在 面板設置-許可證 界面導入許可證" + +#task +TaskStart: "{{.name}} 開始 [START]" +TaskEnd: "{{.name}} 結束 [COMPLETED]" +TaskFailed: "{{.name}} 失敗: {{.err}}" +TaskTimeout: "{{.name}} 逾時" +TaskSuccess: "{{.name}} 成功" +TaskRetry: "開始第 {{.name}} 次重試" +SubTaskStart: "開始 {{.name}}" diff --git a/agent/i18n/lang/zh.yaml b/agent/i18n/lang/zh.yaml new file mode 100644 index 000000000..ff3bb1346 --- /dev/null +++ b/agent/i18n/lang/zh.yaml @@ -0,0 +1,210 @@ +ErrInvalidParams: "请求参数错误: {{ .detail }}" +ErrTokenParse: "Token 生成错误: {{ .detail }}" +ErrInitialPassword: "原密码错误" +ErrInternalServer: "服务内部错误: {{ .detail }}" +ErrRecordExist: "记录已存在" +ErrRecordNotFound: "记录未能找到" +ErrStructTransform: "类型转换失败: {{ .detail }}" +ErrNotLogin: "用户未登录: {{ .detail }}" +ErrPasswordExpired: "当前密码已过期: {{ .detail }}" +ErrNotSupportType: "系统暂不支持当前类型: {{ .detail }}" + +#common +ErrNameIsExist: "名称已存在" +ErrDemoEnvironment: "演示服务器,禁止此操作!" +ErrCmdTimeout: "命令执行超时!" +ErrCmdIllegal: "执行命令中存在不合法字符,请修改后重试!" +ErrPortExist: '{{ .port }} 端口已被 {{ .type }} [{{ .name }}] 占用' +TYPE_APP: "应用" +TYPE_RUNTIME: "运行环境" +TYPE_DOMAIN: "域名" +ErrTypePort: '端口 {{ .name }} 格式错误' +ErrTypePortRange: '端口范围需要在 1-65535 之间' +Success: "成功" +Failed: "失败" +SystemRestart: "系统重启导致任务中断" + +#app +ErrPortInUsed: "{{ .detail }} 端口已被占用!" +ErrAppLimit: "应用超出安装数量限制" +ErrAppRequired: "请先安装 {{ .detail }} 应用" +ErrNotInstall: "应用未安装" +ErrPortInOtherApp: "{{ .port }} 端口已被应用 {{ .apps }} 占用!" +ErrDbUserNotValid: "存量数据库,用户名密码不匹配!" +ErrDockerComposeNotValid: "docker-compose 文件格式错误" +ErrUpdateBuWebsite: '应用更新成功,但是网站配置文件修改失败,请检查配置!' +Err1PanelNetworkFailed: '默认容器网络创建失败!{{ .detail }}' +ErrFileParse: '应用 docker-compose 文件解析失败!' +ErrInstallDirNotFound: '安装目录不存在' +AppStoreIsUpToDate: '应用商店已经是最新版本' +LocalAppVersionNull: '{{.name}} 应用未同步到版本!无法添加到应用列表' +LocalAppVersionErr: '{{.name}} 同步版本 {{.version}} 失败!{{.err}}' +ErrFileNotFound: '{{.name}} 文件不存在' +ErrFileParseApp: '{{.name}} 文件解析失败 {{.err}}' +ErrAppDirNull: '版本文件夹不存在' +LocalAppErr: "应用 {{.name}} 同步失败!{{.err}}" +ErrContainerName: "容器名称已存在" +ErrAppSystemRestart: "1Panel 重启导致任务终止" +ErrCreateHttpClient: "创建HTTP请求失败 {{.err}}" +ErrHttpReqTimeOut: "请求超时 {{.err}}" +ErrHttpReqFailed: "请求失败 {{.err}}" +ErrHttpReqNotFound: "文件不存在" +ErrNoSuchHost: "网络连接失败" +ErrImagePullTimeOut: '镜像拉取超时' +ErrContainerNotFound: '{{ .name }} 容器不存在' +ErrContainerMsg: '{{ .name }} 容器异常,具体请在容器页面查看日志' +ErrAppBackup: '{{ .name }} 应用备份失败 err {{.err}}' +ErrImagePull: '镜像拉取失败 {{.err}}' +ErrVersionTooLow: '当前 1Panel 版本过低,无法更新应用商店,请升级版本之后操作' +ErrAppNameExist: '应用名称已存在' +AppStoreIsSyncing: '应用商店正在同步中,请稍后再试' +ErrGetCompose: "docker-compose.yml 文件获取失败!{{ .detail }}" +ErrAppWarn: "状态异常,请查看日志" +ErrAppParamKey: "参数 {{ .name }} 字段异常" +ErrAppUpgrade: "应用 {{ .name }} 升级失败 {{ .err }}" +AppRecover: "应用 {{ .name }} 回滚 " +PullImageStart: "开始拉取镜像 {{ .name }}" +PullImageSuccess: "镜像拉取成功" +UpgradeAppStart: "开始升级应用 {{ .name }}" +UpgradeAppSuccess: "应用 {{ .name }} 升级成功" + +#file +ErrFileCanNotRead: "此文件不支持预览" +ErrFileToLarge: "文件超过10M,无法打开" +ErrPathNotFound: "目录不存在" +ErrMovePathFailed: "目标路径不能包含原路径!" +ErrLinkPathNotFound: "目标路径不存在!" +ErrFileIsExist: "文件或文件夹已存在!" +ErrFileUpload: "{{ .name }} 上传文件失败 {{ .detail}}" +ErrFileDownloadDir: "不支持下载文件夹" +ErrCmdNotFound: "{{ .name}} 命令不存在,请先在宿主机安装此命令" +ErrSourcePathNotFound: "源目录不存在" +ErrFavoriteExist: "已收藏此路径" +ErrInvalidChar: "禁止使用非法字符" + +#website +ErrDomainIsExist: "域名已存在" +ErrAliasIsExist: "代号已存在" +ErrAppDelete: '其他网站使用此应用,无法删除' +ErrGroupIsUsed: '分组正在使用中,无法删除' +ErrBackupMatch: '该备份文件与当前网站部分数据不匹配 {{ .detail}}' +ErrBackupExist: '该备份文件对应部分源数据不存在 {{ .detail}}' +ErrPHPResource: '本地运行环境不支持切换!' +ErrPathPermission: 'index 目录下检测到非 1000:1000 权限文件夹,可能导致网站访问 Access denied 错误,请点击上方保存按钮' +ErrDomainIsUsed: "域名已被网站【{{ .name }}】使用" +ErrDomainFormat: "{{ .name }} 域名格式不正确" +ErrDefaultAlias: "default 为保留代号,请使用其他代号" + +#ssl +ErrSSLCannotDelete: "{{ .name }} 证书正在被网站使用,无法删除" +ErrAccountCannotDelete: "账号关联证书,无法删除" +ErrSSLApply: "证书续签成功,openresty reload失败,请检查配置!" +ErrEmailIsExist: '邮箱已存在' +ErrSSLKeyNotFound: '私钥文件不存在' +ErrSSLCertificateNotFound: '证书文件不存在' +ErrSSLKeyFormat: '私钥文件校验失败' +ErrSSLCertificateFormat: '证书文件格式错误,请使用 pem 格式' +ErrEabKidOrEabHmacKeyCannotBlank: 'EabKid 或 EabHmacKey 不能为空' +ErrOpenrestyNotFound: 'Http 模式需要首先安装 Openresty' +ApplySSLStart: '开始申请证书,域名 [{{ .domain }}] 申请方式 [{{ .type }}] ' +dnsAccount: "DNS 自动" +dnsManual: "DNS 手动" +http: "HTTP" +ApplySSLFailed: '申请 [{{ .domain }}] 证书失败, {{.detail}} ' +ApplySSLSuccess: '申请 [{{ .domain }}] 证书成功!!' +DNSAccountName: 'DNS 账号 [{{ .name }}] 厂商 [{{.type}}]' +PushDirLog: '证书推送到目录 [{{ .path }}] {{ .status }}' +ErrDeleteCAWithSSL: "当前机构下存在已签发证书,无法删除" +ErrDeleteWithPanelSSL: "面板 SSL 配置使用此证书,无法删除" +ErrDefaultCA: "默认机构不能删除" +ApplyWebSiteSSLLog: "开始更新 {{ .name }} 网站证书" +ErrUpdateWebsiteSSL: "{{ .name }} 网站更新证书失败: {{ .err }}" +ApplyWebSiteSSLSuccess: "更新网站证书成功" +ErrExecShell: "执行脚本失败 {{ .err }}" +ExecShellStart: "开始执行脚本" +ExecShellSuccess: "脚本执行成功" + +#mysql +ErrUserIsExist: "当前用户已存在,请重新输入" +ErrDatabaseIsExist: "当前数据库已存在,请重新输入" +ErrExecTimeOut: "SQL 执行超时,请检查数据库" +ErrRemoteExist: "远程数据库已存在该名称,请修改后重试" +ErrLocalExist: "本地数据库已存在该名称,请修改后重试" + +#redis +ErrTypeOfRedis: "恢复文件类型与当前持久化方式不符,请修改后重试" + +#container +ErrInUsed: "{{ .detail }} 正被使用,无法删除" +ErrObjectInUsed: "该对象正被使用,无法删除" +ErrPortRules: "端口数目不匹配,请重新输入!" +ErrPgImagePull: "镜像拉取超时,请配置镜像加速或手动拉取 postgres:16.0-alpine 镜像后重试" + +#runtime +ErrDirNotFound: "build 文件夹不存在!请检查文件完整性!" +ErrFileNotExist: "{{ .detail }} 文件不存在!请检查源文件完整性!" +ErrImageBuildErr: "镜像 build 失败" +ErrImageExist: "镜像已存在!" +ErrDelWithWebsite: "运行环境已经关联网站,无法删除" +ErrRuntimeStart: "启动失败" +ErrPackageJsonNotFound: "package.json 文件不存在" +ErrScriptsNotFound: "没有在 package.json 中找到 scripts 配置项" +ErrContainerNameNotFound: "无法获取容器名称,请检查 .env 文件" +ErrNodeModulesNotFound: "node_modules 文件夹不存在!请编辑运行环境或者等待运行环境启动成功" + +#setting +ErrBackupInUsed: "该备份账号已在计划任务中使用,无法删除" +ErrBackupCheck: "备份账号测试连接失败 {{ .err}}" +ErrOSSConn: "无法获取最新版本,请确认服务器是否能够连接外部网络。" +ErrEntrance: "安全入口信息错误,请检查后重试!" + +#tool +ErrConfigNotFound: "配置文件不存在" +ErrConfigParse: "配置文件格式有误" +ErrConfigIsNull: "配置文件不允许为空" +ErrConfigDirNotFound: "运行目录不存在" +ErrConfigAlreadyExist: "已存在同名配置文件" +ErrUserFindErr: "用户 {{ .name }} 查找失败 {{ .err }}" + +#ssh +ErrFirewall: "当前未检测到系统 firewalld 或 ufw 服务,请检查后重试!" + +#cronjob +ErrBashExecute: "脚本执行错误,请在任务输出文本域中查看具体信息。" +ErrCutWebsiteLog: "{{ .name }} 网站日志切割失败,错误 {{ .err }}" +CutWebsiteLogSuccess: "{{ .name }} 网站日志切割成功,备份路径 {{ .path }}" + +#toolbox +ErrNotExistUser: "当前用户不存在,请修改后重试!" +ErrBanAction: "设置失败,当前 {{ .name }} 服务不可用,请检查后重试!" +ErrClamdscanNotFound: "未检测到 clamdscan 命令,请参考文档安装!" + +#waf +ErrScope: "不支持修改此配置" +ErrStateChange: "状态修改失败" +ErrRuleExist: "规则已存在" +ErrRuleNotExist: "规则不存在" +ErrParseIP: "IP 格式错误" +ErrDefaultIP: "default 为保留名称,请更换其他名称" +ErrGroupInUse: "IP 组被黑/白名单使用,无法删除" +ErrGroupExist: "IP 组名称已存在" +ErrIPRange: "IP 范围错误" +ErrIPExist: "IP 已存在" + +#license +ErrLicense: "许可证格式错误,请检查后重试!" +ErrLicenseCheck: "许可证校验失败,请检查后重试!" +ErrLicenseSave: "许可证信息保存失败,错误 {{ .err }},请重试!" +ErrLicenseSync: "许可证信息同步失败,数据库中未检测到许可证信息!" +ErrXpackNotFound: "该部分为专业版功能,请先在 面板设置-许可证 界面导入许可证" +ErrXpackNotActive: "该部分为专业版功能,请先在 面板设置-许可证 界面同步许可证状态" +ErrXpackOutOfDate: "当前许可证已过期,请重新在 面板设置-许可证 界面导入许可证" + +#task +TaskStart: "{{.name}} 开始 [START]" +TaskEnd: "{{.name}} 结束 [COMPLETED]" +TaskFailed: "{{.name}} 失败: {{.err}}" +TaskTimeout: "{{.name}} 超时" +TaskSuccess: "{{.name}} 成功" +TaskRetry: "开始第 {{.name}} 次重试" +SubTaskStart: "开始 {{.name}}" diff --git a/agent/init/app/app.go b/agent/init/app/app.go new file mode 100644 index 000000000..c7989a8a1 --- /dev/null +++ b/agent/init/app/app.go @@ -0,0 +1,47 @@ +package app + +import ( + "path" + + "github.com/1Panel-dev/1Panel/agent/utils/docker" + "github.com/1Panel-dev/1Panel/agent/utils/firewall" + + "github.com/1Panel-dev/1Panel/agent/constant" + "github.com/1Panel-dev/1Panel/agent/global" + "github.com/1Panel-dev/1Panel/agent/utils/files" +) + +func Init() { + constant.DataDir = global.CONF.System.DataDir + constant.ResourceDir = path.Join(constant.DataDir, "resource") + constant.AppResourceDir = path.Join(constant.ResourceDir, "apps") + constant.AppInstallDir = path.Join(constant.DataDir, "apps") + constant.RuntimeDir = path.Join(constant.DataDir, "runtime") + + constant.LocalAppResourceDir = path.Join(constant.AppResourceDir, "local") + constant.LocalAppInstallDir = path.Join(constant.AppInstallDir, "local") + constant.RemoteAppResourceDir = path.Join(constant.AppResourceDir, "remote") + + constant.LogDir = path.Join(global.CONF.System.DataDir, "log") + constant.SSLLogDir = path.Join(constant.LogDir, "ssl") + + dirs := []string{constant.DataDir, constant.ResourceDir, constant.AppResourceDir, constant.AppInstallDir, + global.CONF.System.Backup, constant.RuntimeDir, constant.LocalAppResourceDir, constant.RemoteAppResourceDir, constant.SSLLogDir} + + fileOp := files.NewFileOp() + for _, dir := range dirs { + createDir(fileOp, dir) + } + + _ = docker.CreateDefaultDockerNetwork() + + if f, err := firewall.NewFirewallClient(); err == nil { + _ = f.EnableForward() + } +} + +func createDir(fileOp files.FileOp, dirPath string) { + if !fileOp.Stat(dirPath) { + _ = fileOp.CreateDir(dirPath, 0755) + } +} diff --git a/agent/init/business/business.go b/agent/init/business/business.go new file mode 100644 index 000000000..c2cc61238 --- /dev/null +++ b/agent/init/business/business.go @@ -0,0 +1,40 @@ +package business + +import ( + "github.com/1Panel-dev/1Panel/agent/app/service" + "github.com/1Panel-dev/1Panel/agent/constant" + "github.com/1Panel-dev/1Panel/agent/global" +) + +func Init() { + go syncApp() + go syncInstalledApp() + go syncRuntime() + go syncSSL() +} + +func syncApp() { + _ = service.NewISettingService().Update("AppStoreSyncStatus", constant.SyncSuccess) + if err := service.NewIAppService().SyncAppListFromRemote(); err != nil { + global.LOG.Errorf("App Store synchronization failed") + return + } +} + +func syncInstalledApp() { + if err := service.NewIAppInstalledService().SyncAll(true); err != nil { + global.LOG.Errorf("sync installed app error: %s", err.Error()) + } +} + +func syncRuntime() { + if err := service.NewRuntimeService().SyncForRestart(); err != nil { + global.LOG.Errorf("sync runtime status error : %s", err.Error()) + } +} + +func syncSSL() { + if err := service.NewIWebsiteSSLService().SyncForRestart(); err != nil { + global.LOG.Errorf("sync ssl status error : %s", err.Error()) + } +} diff --git a/agent/init/cache/badger_db/badger_db.go b/agent/init/cache/badger_db/badger_db.go new file mode 100644 index 000000000..a1508503a --- /dev/null +++ b/agent/init/cache/badger_db/badger_db.go @@ -0,0 +1,79 @@ +package badger_db + +import ( + "fmt" + "time" + + "github.com/dgraph-io/badger/v4" +) + +type Cache struct { + db *badger.DB +} + +func NewCacheDB(db *badger.DB) *Cache { + return &Cache{ + db: db, + } +} + +func (c *Cache) Set(key string, value interface{}) error { + err := c.db.Update(func(txn *badger.Txn) error { + v := []byte(fmt.Sprintf("%v", value)) + return txn.Set([]byte(key), v) + }) + return err +} + +func (c *Cache) Del(key string) error { + err := c.db.Update(func(txn *badger.Txn) error { + return txn.Delete([]byte(key)) + }) + return err +} + +func (c *Cache) Clean() error { + return c.db.DropAll() +} + +func (c *Cache) Get(key string) ([]byte, error) { + var result []byte + err := c.db.View(func(txn *badger.Txn) error { + item, err := txn.Get([]byte(key)) + if err != nil { + return err + } + err = item.Value(func(val []byte) error { + result = append([]byte{}, val...) + return nil + }) + return err + }) + return result, err +} + +func (c *Cache) SetWithTTL(key string, value interface{}, duration time.Duration) error { + err := c.db.Update(func(txn *badger.Txn) error { + v := []byte(fmt.Sprintf("%v", value)) + e := badger.NewEntry([]byte(key), v).WithTTL(duration) + return txn.SetEntry(e) + }) + return err +} + +func (c *Cache) PrefixScanKey(prefixStr string) ([]string, error) { + var res []string + err := c.db.View(func(txn *badger.Txn) error { + it := txn.NewIterator(badger.DefaultIteratorOptions) + defer it.Close() + prefix := []byte(prefixStr) + for it.Seek(prefix); it.ValidForPrefix(prefix); it.Next() { + item := it.Item() + k := item.Key() + res = append(res, string(k)) + return nil + } + return nil + }) + return res, err +} diff --git a/agent/init/cache/cache.go b/agent/init/cache/cache.go new file mode 100644 index 000000000..6364e1625 --- /dev/null +++ b/agent/init/cache/cache.go @@ -0,0 +1,56 @@ +package cache + +import ( + "time" + + "github.com/1Panel-dev/1Panel/agent/global" + "github.com/1Panel-dev/1Panel/agent/init/cache/badger_db" + "github.com/dgraph-io/badger/v4" +) + +func Init() { + c := global.CONF.System.Cache + + options := badger.Options{ + Dir: c, + ValueDir: c, + ValueLogFileSize: 64 << 20, + ValueLogMaxEntries: 10 << 20, + VLogPercentile: 0.1, + + MemTableSize: 32 << 20, + BaseTableSize: 2 << 20, + BaseLevelSize: 10 << 20, + TableSizeMultiplier: 2, + LevelSizeMultiplier: 10, + MaxLevels: 7, + NumGoroutines: 4, + MetricsEnabled: true, + NumCompactors: 2, + NumLevelZeroTables: 5, + NumLevelZeroTablesStall: 15, + NumMemtables: 1, + BloomFalsePositive: 0.01, + BlockSize: 2 * 1024, + SyncWrites: false, + NumVersionsToKeep: 1, + CompactL0OnClose: false, + VerifyValueChecksum: false, + BlockCacheSize: 32 << 20, + IndexCacheSize: 0, + ZSTDCompressionLevel: 1, + EncryptionKey: []byte{}, + EncryptionKeyRotationDuration: 10 * 24 * time.Hour, // Default 10 days. + DetectConflicts: true, + NamespaceOffset: -1, + } + + cache, err := badger.Open(options) + if err != nil { + panic(err) + } + _ = cache.DropAll() + global.CacheDb = cache + global.CACHE = badger_db.NewCacheDB(cache) + global.LOG.Info("init cache successfully") +} diff --git a/agent/init/db/db.go b/agent/init/db/db.go new file mode 100644 index 000000000..cd8c79bb2 --- /dev/null +++ b/agent/init/db/db.go @@ -0,0 +1,94 @@ +package db + +import ( + "fmt" + "log" + "os" + "path" + "time" + + "github.com/1Panel-dev/1Panel/agent/global" + "github.com/glebarez/sqlite" + "gorm.io/gorm" + "gorm.io/gorm/logger" +) + +func Init() { + if _, err := os.Stat(global.CONF.System.DbPath); err != nil { + if err := os.MkdirAll(global.CONF.System.DbPath, os.ModePerm); err != nil { + panic(fmt.Errorf("init db dir failed, err: %v", err)) + } + } + fullPath := global.CONF.System.DbPath + "/" + global.CONF.System.DbFile + if _, err := os.Stat(fullPath); err != nil { + f, err := os.Create(fullPath) + if err != nil { + panic(fmt.Errorf("init db file failed, err: %v", err)) + } + _ = f.Close() + } + + newLogger := logger.New( + log.New(os.Stdout, "\r\n", log.LstdFlags), + logger.Config{ + SlowThreshold: time.Second, + LogLevel: logger.Silent, + IgnoreRecordNotFoundError: true, + Colorful: false, + }, + ) + initMonitorDB(newLogger) + + db, err := gorm.Open(sqlite.Open(fullPath), &gorm.Config{ + DisableForeignKeyConstraintWhenMigrating: true, + Logger: newLogger, + }) + if err != nil { + panic(err) + } + _ = db.Exec("PRAGMA journal_mode = WAL;") + sqlDB, dbError := db.DB() + if dbError != nil { + panic(dbError) + } + sqlDB.SetConnMaxIdleTime(10) + sqlDB.SetMaxOpenConns(100) + sqlDB.SetConnMaxLifetime(time.Hour) + + global.DB = db + global.LOG.Info("init db successfully") +} + +func initMonitorDB(newLogger logger.Interface) { + if _, err := os.Stat(global.CONF.System.DbPath); err != nil { + if err := os.MkdirAll(global.CONF.System.DbPath, os.ModePerm); err != nil { + panic(fmt.Errorf("init db dir failed, err: %v", err)) + } + } + fullPath := path.Join(global.CONF.System.DbPath, "monitor.db") + if _, err := os.Stat(fullPath); err != nil { + f, err := os.Create(fullPath) + if err != nil { + panic(fmt.Errorf("init db file failed, err: %v", err)) + } + _ = f.Close() + } + + db, err := gorm.Open(sqlite.Open(fullPath), &gorm.Config{ + DisableForeignKeyConstraintWhenMigrating: true, + Logger: newLogger, + }) + if err != nil { + panic(err) + } + sqlDB, dbError := db.DB() + if dbError != nil { + panic(dbError) + } + sqlDB.SetConnMaxIdleTime(10) + sqlDB.SetMaxOpenConns(100) + sqlDB.SetConnMaxLifetime(time.Hour) + + global.MonitorDB = db + global.LOG.Info("init monitor db successfully") +} diff --git a/agent/init/hook/hook.go b/agent/init/hook/hook.go new file mode 100644 index 000000000..82e40ddae --- /dev/null +++ b/agent/init/hook/hook.go @@ -0,0 +1,147 @@ +package hook + +import ( + "encoding/base64" + "encoding/json" + "os" + "path" + + "github.com/1Panel-dev/1Panel/agent/app/model" + "github.com/1Panel-dev/1Panel/agent/app/repo" + "github.com/1Panel-dev/1Panel/agent/constant" + "github.com/1Panel-dev/1Panel/agent/global" +) + +func Init() { + settingRepo := repo.NewISettingRepo() + OneDriveID, err := settingRepo.Get(settingRepo.WithByKey("OneDriveID")) + if err != nil { + global.LOG.Errorf("load onedrive info from setting failed, err: %v", err) + } + idItem, _ := base64.StdEncoding.DecodeString(OneDriveID.Value) + global.CONF.System.OneDriveID = string(idItem) + OneDriveSc, err := settingRepo.Get(settingRepo.WithByKey("OneDriveSc")) + if err != nil { + global.LOG.Errorf("load onedrive info from setting failed, err: %v", err) + } + scItem, _ := base64.StdEncoding.DecodeString(OneDriveSc.Value) + global.CONF.System.OneDriveSc = string(scItem) + + if _, err := settingRepo.Get(settingRepo.WithByKey("SystemStatus")); err != nil { + _ = settingRepo.Create("SystemStatus", "Free") + } + if err := settingRepo.Update("SystemStatus", "Free"); err != nil { + global.LOG.Fatalf("init service before start failed, err: %v", err) + } + + handleCronjobStatus() + handleSnapStatus() + loadLocalDir() + initDir() +} + +func handleSnapStatus() { + msgFailed := "the task was interrupted due to the restart of the 1panel service" + _ = global.DB.Model(&model.Snapshot{}).Where("status = ?", "OnSaveData"). + Updates(map[string]interface{}{"status": constant.StatusSuccess}).Error + + _ = global.DB.Model(&model.Snapshot{}).Where("status = ?", constant.StatusWaiting). + Updates(map[string]interface{}{ + "status": constant.StatusFailed, + "message": msgFailed, + }).Error + + _ = global.DB.Model(&model.Snapshot{}).Where("recover_status = ?", constant.StatusWaiting). + Updates(map[string]interface{}{ + "recover_status": constant.StatusFailed, + "recover_message": msgFailed, + }).Error + + _ = global.DB.Model(&model.Snapshot{}).Where("rollback_status = ?", constant.StatusWaiting). + Updates(map[string]interface{}{ + "rollback_status": constant.StatusFailed, + "rollback_message": msgFailed, + }).Error + + snapRepo := repo.NewISnapshotRepo() + + status, _ := snapRepo.GetStatusList() + for _, item := range status { + updates := make(map[string]interface{}) + if item.Panel == constant.StatusRunning { + updates["panel"] = constant.StatusFailed + } + if item.PanelInfo == constant.StatusRunning { + updates["panel_info"] = constant.StatusFailed + } + if item.DaemonJson == constant.StatusRunning { + updates["daemon_json"] = constant.StatusFailed + } + if item.AppData == constant.StatusRunning { + updates["app_data"] = constant.StatusFailed + } + if item.PanelData == constant.StatusRunning { + updates["panel_data"] = constant.StatusFailed + } + if item.BackupData == constant.StatusRunning { + updates["backup_data"] = constant.StatusFailed + } + if item.Compress == constant.StatusRunning { + updates["compress"] = constant.StatusFailed + } + if item.Upload == constant.StatusUploading { + updates["upload"] = constant.StatusFailed + } + if len(updates) != 0 { + _ = snapRepo.UpdateStatus(item.ID, updates) + } + } +} + +func handleCronjobStatus() { + _ = global.DB.Model(&model.JobRecords{}).Where("status = ?", constant.StatusWaiting). + Updates(map[string]interface{}{ + "status": constant.StatusFailed, + "message": "the task was interrupted due to the restart of the 1panel service", + }).Error +} + +func loadLocalDir() { + var backup model.BackupAccount + _ = global.DB.Where("type = ?", "LOCAL").First(&backup).Error + if backup.ID == 0 { + global.LOG.Errorf("no such backup account `%s` in db", "LOCAL") + return + } + varMap := make(map[string]interface{}) + if err := json.Unmarshal([]byte(backup.Vars), &varMap); err != nil { + global.LOG.Errorf("json unmarshal backup.Vars: %v failed, err: %v", backup.Vars, err) + return + } + if _, ok := varMap["dir"]; !ok { + global.LOG.Error("load local backup dir failed") + return + } + baseDir, ok := varMap["dir"].(string) + if ok { + if _, err := os.Stat(baseDir); err != nil && os.IsNotExist(err) { + if err = os.MkdirAll(baseDir, os.ModePerm); err != nil { + global.LOG.Errorf("mkdir %s failed, err: %v", baseDir, err) + return + } + } + global.CONF.System.Backup = baseDir + return + } + global.LOG.Errorf("error type dir: %T", varMap["dir"]) +} + +func initDir() { + composePath := path.Join(global.CONF.System.BaseDir, "1panel/docker/compose/") + if _, err := os.Stat(composePath); err != nil && os.IsNotExist(err) { + if err = os.MkdirAll(composePath, os.ModePerm); err != nil { + global.LOG.Errorf("mkdir %s failed, err: %v", composePath, err) + return + } + } +} diff --git a/agent/init/log/log.go b/agent/init/log/log.go new file mode 100644 index 000000000..9e8e5c5c6 --- /dev/null +++ b/agent/init/log/log.go @@ -0,0 +1,68 @@ +package log + +import ( + "fmt" + "io" + "os" + "strings" + "time" + + "github.com/1Panel-dev/1Panel/agent/log" + + "github.com/1Panel-dev/1Panel/agent/configs" + "github.com/1Panel-dev/1Panel/agent/global" + + "github.com/sirupsen/logrus" +) + +const ( + TimeFormat = "2006-01-02 15:04:05" + FileTImeFormat = "2006-01-02" + RollingTimePattern = "0 0 * * *" +) + +func Init() { + l := logrus.New() + setOutput(l, global.CONF.LogConfig) + global.LOG = l + global.LOG.Info("init logger successfully") +} + +func setOutput(logger *logrus.Logger, config configs.LogConfig) { + writer, err := log.NewWriterFromConfig(&log.Config{ + LogPath: global.CONF.System.LogPath, + FileName: config.LogName, + TimeTagFormat: FileTImeFormat, + MaxRemain: config.MaxBackup, + RollingTimePattern: RollingTimePattern, + LogSuffix: config.LogSuffix, + }) + if err != nil { + panic(err) + } + level, err := logrus.ParseLevel(config.Level) + if err != nil { + panic(err) + } + fileAndStdoutWriter := io.MultiWriter(writer, os.Stdout) + + logger.SetOutput(fileAndStdoutWriter) + logger.SetLevel(level) + logger.SetFormatter(new(MineFormatter)) +} + +type MineFormatter struct{} + +func (s *MineFormatter) Format(entry *logrus.Entry) ([]byte, error) { + detailInfo := "" + if entry.Caller != nil { + function := strings.ReplaceAll(entry.Caller.Function, "github.com/1Panel-dev/1Panel/agent/", "") + detailInfo = fmt.Sprintf("(%s: %d)", function, entry.Caller.Line) + } + if len(entry.Data) == 0 { + msg := fmt.Sprintf("[%s] [%s] %s %s \n", time.Now().Format(TimeFormat), strings.ToUpper(entry.Level.String()), entry.Message, detailInfo) + return []byte(msg), nil + } + msg := fmt.Sprintf("[%s] [%s] %s %s {%v} \n", time.Now().Format(TimeFormat), strings.ToUpper(entry.Level.String()), entry.Message, detailInfo, entry.Data) + return []byte(msg), nil +} diff --git a/agent/init/migration/migrate.go b/agent/init/migration/migrate.go new file mode 100644 index 000000000..1d05e8ab7 --- /dev/null +++ b/agent/init/migration/migrate.go @@ -0,0 +1,26 @@ +package migration + +import ( + "github.com/1Panel-dev/1Panel/agent/global" + "github.com/1Panel-dev/1Panel/agent/init/migration/migrations" + + "github.com/go-gormigrate/gormigrate/v2" +) + +func Init() { + m := gormigrate.New(global.DB, gormigrate.DefaultOptions, []*gormigrate.Migration{ + migrations.AddTable, + migrations.InitHost, + migrations.InitSetting, + migrations.InitBackupAccount, + migrations.InitImageRepo, + migrations.InitDefaultGroup, + migrations.InitDefaultCA, + migrations.InitPHPExtensions, + }) + if err := m.Migrate(); err != nil { + global.LOG.Error(err) + panic(err) + } + global.LOG.Info("Migration run successfully") +} diff --git a/agent/init/migration/migrations/init.go b/agent/init/migration/migrations/init.go new file mode 100644 index 000000000..e1b5db09c --- /dev/null +++ b/agent/init/migration/migrations/init.go @@ -0,0 +1,249 @@ +package migrations + +import ( + "fmt" + + "github.com/1Panel-dev/1Panel/agent/app/dto/request" + "github.com/1Panel-dev/1Panel/agent/app/model" + "github.com/1Panel-dev/1Panel/agent/app/service" + "github.com/1Panel-dev/1Panel/agent/constant" + "github.com/1Panel-dev/1Panel/agent/global" + "github.com/1Panel-dev/1Panel/agent/utils/common" + + "github.com/go-gormigrate/gormigrate/v2" + "gorm.io/gorm" +) + +var AddTable = &gormigrate.Migration{ + ID: "20240722-add-table", + Migrate: func(tx *gorm.DB) error { + return tx.AutoMigrate( + &model.AppDetail{}, + &model.AppInstallResource{}, + &model.AppInstall{}, + &model.AppTag{}, + &model.Tag{}, + &model.App{}, + &model.BackupAccount{}, + &model.BackupRecord{}, + &model.Clam{}, + &model.Command{}, + &model.ComposeTemplate{}, + &model.Compose{}, + &model.Cronjob{}, + &model.Database{}, + &model.DatabaseMysql{}, + &model.DatabasePostgresql{}, + &model.Favorite{}, + &model.Forward{}, + &model.Firewall{}, + &model.Ftp{}, + &model.Group{}, + &model.Host{}, + &model.ImageRepo{}, + &model.JobRecords{}, + &model.MonitorBase{}, + &model.MonitorIO{}, + &model.MonitorNetwork{}, + &model.PHPExtensions{}, + &model.RedisCommand{}, + &model.Runtime{}, + &model.Setting{}, + &model.Snapshot{}, + &model.SnapshotStatus{}, + &model.Tag{}, + &model.Website{}, + &model.WebsiteAcmeAccount{}, + &model.WebsiteCA{}, + &model.WebsiteDnsAccount{}, + &model.WebsiteDomain{}, + &model.WebsiteSSL{}, + ) + }, +} + +var InitHost = &gormigrate.Migration{ + ID: "20240722-init-host", + Migrate: func(tx *gorm.DB) error { + group := model.Group{ + Name: "default", Type: "host", IsDefault: true, + } + if err := tx.Create(&group).Error; err != nil { + return err + } + host := model.Host{ + Name: "localhost", Addr: "127.0.0.1", User: "root", Port: 22, AuthMode: "password", GroupID: group.ID, + } + if err := tx.Create(&host).Error; err != nil { + return err + } + return nil + }, +} + +var InitSetting = &gormigrate.Migration{ + ID: "20240722-init-setting", + Migrate: func(tx *gorm.DB) error { + if err := tx.Create(&model.Setting{Key: "SystemIP", Value: ""}).Error; err != nil { + return err + } + if err := tx.Create(&model.Setting{Key: "DockerSockPath", Value: "unix:///var/run/docker.sock"}).Error; err != nil { + return err + } + if err := tx.Create(&model.Setting{Key: "SystemVersion", Value: global.CONF.System.Version}).Error; err != nil { + return err + } + if err := tx.Create(&model.Setting{Key: "SystemStatus", Value: "Free"}).Error; err != nil { + return err + } + + if err := tx.Create(&model.Setting{Key: "LocalTime", Value: ""}).Error; err != nil { + return err + } + if err := tx.Create(&model.Setting{Key: "TimeZone", Value: common.LoadTimeZoneByCmd()}).Error; err != nil { + return err + } + if err := tx.Create(&model.Setting{Key: "NtpSite", Value: "pool.ntp.org"}).Error; err != nil { + return err + } + + if err := tx.Create(&model.Setting{Key: "DefaultNetwork", Value: "all"}).Error; err != nil { + return err + } + if err := tx.Create(&model.Setting{Key: "LastCleanTime", Value: ""}).Error; err != nil { + return err + } + if err := tx.Create(&model.Setting{Key: "LastCleanSize", Value: ""}).Error; err != nil { + return err + } + if err := tx.Create(&model.Setting{Key: "LastCleanData", Value: ""}).Error; err != nil { + return err + } + + if err := tx.Create(&model.Setting{Key: "MonitorStatus", Value: "enable"}).Error; err != nil { + return err + } + if err := tx.Create(&model.Setting{Key: "MonitorStoreDays", Value: "7"}).Error; err != nil { + return err + } + if err := tx.Create(&model.Setting{Key: "MonitorInterval", Value: "5"}).Error; err != nil { + return err + } + + if err := tx.Create(&model.Setting{Key: "AppStoreVersion", Value: ""}).Error; err != nil { + return err + } + if err := tx.Create(&model.Setting{Key: "AppStoreSyncStatus", Value: "SyncSuccess"}).Error; err != nil { + return err + } + if err := tx.Create(&model.Setting{Key: "AppStoreLastModified", Value: "0"}).Error; err != nil { + return err + } + + if err := tx.Create(&model.Setting{Key: "OneDriveID", Value: "MDEwOTM1YTktMWFhOS00ODU0LWExZGMtNmU0NWZlNjI4YzZi"}).Error; err != nil { + return err + } + if err := tx.Create(&model.Setting{Key: "OneDriveSc", Value: "akpuOFF+YkNXOU1OLWRzS1ZSRDdOcG1LT2ZRM0RLNmdvS1RkVWNGRA=="}).Error; err != nil { + return err + } + + if err := tx.Create(&model.Setting{Key: "FileRecycleBin", Value: "enable"}).Error; err != nil { + return err + } + if err := tx.Create(&model.Setting{Key: "SnapshotIgnore", Value: "*.sock"}).Error; err != nil { + return err + } + return nil + }, +} + +var InitBackupAccount = &gormigrate.Migration{ + ID: "20240722-init-backup", + Migrate: func(tx *gorm.DB) error { + item := &model.BackupAccount{ + Type: "LOCAL", + Vars: fmt.Sprintf("{\"dir\":\"%s\"}", global.CONF.System.Backup), + } + if err := tx.Create(item).Error; err != nil { + return err + } + return nil + }, +} + +var InitImageRepo = &gormigrate.Migration{ + ID: "20240722-init-imagerepo", + Migrate: func(tx *gorm.DB) error { + item := &model.ImageRepo{ + Name: "Docker Hub", + Protocol: "https", + DownloadUrl: "docker.io", + Status: constant.StatusSuccess, + } + if err := tx.Create(item).Error; err != nil { + return err + } + return nil + }, +} + +var InitDefaultGroup = &gormigrate.Migration{ + ID: "20240722-init-default-group", + Migrate: func(tx *gorm.DB) error { + websiteGroup := &model.Group{ + Name: "默认", + IsDefault: true, + Type: "website", + } + if err := tx.Create(websiteGroup).Error; err != nil { + return err + } + commandGroup := &model.Group{IsDefault: true, Name: "默认", Type: "command"} + if err := tx.Create(commandGroup).Error; err != nil { + return err + } + return nil + }, +} + +var InitDefaultCA = &gormigrate.Migration{ + ID: "20240722-init-default-ca", + Migrate: func(tx *gorm.DB) error { + caService := service.NewIWebsiteCAService() + if _, err := caService.Create(request.WebsiteCACreate{ + CommonName: "1Panel-CA", + Country: "CN", + KeyType: "P256", + Name: "1Panel", + Organization: "FIT2CLOUD", + OrganizationUint: "1Panel", + Province: "Beijing", + City: "Beijing", + }); err != nil { + return err + } + return nil + }, +} + +var InitPHPExtensions = &gormigrate.Migration{ + ID: "20240722-add-php-extensions", + Migrate: func(tx *gorm.DB) error { + 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: "WordPress", Extensions: "exif,igbinary,imagick,intl,zip,apcu,memcached,opcache,redis,bc,image,shmop,mysqli,pdo_mysql,gd"}).Error; err != nil { + return err + } + if err := tx.Create(&model.PHPExtensions{Name: "Flarum", Extensions: "curl,gd,pdo_mysql,mysqli,bz2,exif,yaf,imap"}).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 + }, +} diff --git a/agent/init/router/router.go b/agent/init/router/router.go new file mode 100644 index 000000000..10551cd54 --- /dev/null +++ b/agent/init/router/router.go @@ -0,0 +1,41 @@ +package router + +import ( + "github.com/1Panel-dev/1Panel/agent/i18n" + "github.com/1Panel-dev/1Panel/agent/middleware" + rou "github.com/1Panel-dev/1Panel/agent/router" + "github.com/gin-contrib/gzip" + "github.com/gin-gonic/gin" +) + +var ( + Router *gin.Engine +) + +func setWebStatic(rootRouter *gin.RouterGroup) { + rootRouter.Static("/api/v1/images", "./uploads") + rootRouter.Use(func(c *gin.Context) { + c.Next() + }) +} + +func Routers() *gin.Engine { + Router = gin.Default() + Router.Use(i18n.UseI18n()) + + PublicGroup := Router.Group("") + { + PublicGroup.GET("/health", func(c *gin.Context) { + c.JSON(200, "ok") + }) + PublicGroup.Use(gzip.Gzip(gzip.DefaultCompression)) + setWebStatic(PublicGroup) + } + PrivateGroup := Router.Group("/api/v1") + PrivateGroup.Use(middleware.GlobalLoading()) + for _, router := range rou.RouterGroupApp { + router.InitRouter(PrivateGroup) + } + + return Router +} diff --git a/agent/init/validator/validator.go b/agent/init/validator/validator.go new file mode 100644 index 000000000..bf75f9eff --- /dev/null +++ b/agent/init/validator/validator.go @@ -0,0 +1,65 @@ +package validator + +import ( + "regexp" + "unicode" + + "github.com/1Panel-dev/1Panel/agent/global" + + "github.com/go-playground/validator/v10" +) + +func Init() { + validator := validator.New() + if err := validator.RegisterValidation("name", checkNamePattern); err != nil { + panic(err) + } + if err := validator.RegisterValidation("ip", checkIpPattern); err != nil { + panic(err) + } + if err := validator.RegisterValidation("password", checkPasswordPattern); err != nil { + panic(err) + } + global.VALID = validator +} + +func checkNamePattern(fl validator.FieldLevel) bool { + value := fl.Field().String() + result, err := regexp.MatchString("^[a-zA-Z\u4e00-\u9fa5]{1}[a-zA-Z0-9_\u4e00-\u9fa5]{0,30}$", value) + if err != nil { + global.LOG.Errorf("regexp matchString failed, %v", err) + } + return result +} + +func checkIpPattern(fl validator.FieldLevel) bool { + value := fl.Field().String() + result, err := regexp.MatchString(`^((2(5[0-5]|[0-4]\d))|[0-1]?\d{1,2})(\.((2(5[0-5]|[0-4]\d))|[0-1]?\d{1,2})){3}$`, value) + if err != nil { + global.LOG.Errorf("regexp check ip matchString failed, %v", err) + } + return result +} + +func checkPasswordPattern(fl validator.FieldLevel) bool { + value := fl.Field().String() + if len(value) < 8 || len(value) > 30 { + return false + } + + hasNum := false + hasLetter := false + for _, r := range value { + if unicode.IsLetter(r) && !hasLetter { + hasLetter = true + } + if unicode.IsNumber(r) && !hasNum { + hasNum = true + } + if hasLetter && hasNum { + return true + } + } + + return false +} diff --git a/agent/init/viper/viper.go b/agent/init/viper/viper.go new file mode 100644 index 000000000..734515308 --- /dev/null +++ b/agent/init/viper/viper.go @@ -0,0 +1,90 @@ +package viper + +import ( + "bytes" + "fmt" + "path" + "strings" + + "github.com/1Panel-dev/1Panel/agent/cmd/server/conf" + "github.com/1Panel-dev/1Panel/agent/configs" + "github.com/1Panel-dev/1Panel/agent/global" + "github.com/1Panel-dev/1Panel/agent/utils/cmd" + "github.com/1Panel-dev/1Panel/agent/utils/files" + "github.com/fsnotify/fsnotify" + "github.com/spf13/viper" + "gopkg.in/yaml.v3" +) + +func Init() { + baseDir := "/opt" + mode := "" + version := "v1.0.0" + fileOp := files.NewFileOp() + v := viper.NewWithOptions() + v.SetConfigType("yaml") + + config := configs.ServerConfig{} + if err := yaml.Unmarshal(conf.AppYaml, &config); err != nil { + panic(err) + } + if config.System.Mode != "" { + mode = config.System.Mode + } + if mode == "dev" && fileOp.Stat("/opt/1panel/conf/app.yaml") { + v.SetConfigName("app") + v.AddConfigPath(path.Join("/opt/1panel/conf")) + if err := v.ReadInConfig(); err != nil { + panic(fmt.Errorf("Fatal error config file: %s \n", err)) + } + } else { + baseDir = loadParams("BASE_DIR") + version = loadParams("ORIGINAL_VERSION") + + reader := bytes.NewReader(conf.AppYaml) + if err := v.ReadConfig(reader); err != nil { + panic(fmt.Errorf("Fatal error config file: %s \n", err)) + } + } + v.OnConfigChange(func(e fsnotify.Event) { + if err := v.Unmarshal(&global.CONF); err != nil { + panic(err) + } + }) + serverConfig := configs.ServerConfig{} + if err := v.Unmarshal(&serverConfig); err != nil { + panic(err) + } + if mode == "dev" && fileOp.Stat("/opt/1panel/conf/app.yaml") { + if serverConfig.System.BaseDir != "" { + baseDir = serverConfig.System.BaseDir + } + if serverConfig.System.Version != "" { + version = serverConfig.System.Version + } + } + + global.CONF = serverConfig + global.CONF.System.BaseDir = baseDir + global.CONF.System.IsDemo = v.GetBool("system.is_demo") + global.CONF.System.DataDir = path.Join(global.CONF.System.BaseDir, "1panel") + global.CONF.System.Cache = path.Join(global.CONF.System.DataDir, "cache") + global.CONF.System.Backup = path.Join(global.CONF.System.DataDir, "backup") + global.CONF.System.DbPath = path.Join(global.CONF.System.DataDir, "db") + global.CONF.System.LogPath = path.Join(global.CONF.System.DataDir, "log") + global.CONF.System.TmpDir = path.Join(global.CONF.System.DataDir, "tmp") + global.CONF.System.Version = version + global.Viper = v +} + +func loadParams(param string) string { + stdout, err := cmd.Execf("grep '^%s=' /usr/local/bin/1pctl | cut -d'=' -f2", param) + if err != nil { + panic(err) + } + info := strings.ReplaceAll(stdout, "\n", "") + if len(info) == 0 || info == `""` { + panic(fmt.Sprintf("error `%s` find in /usr/local/bin/1pctl", param)) + } + return info +} diff --git a/agent/log/config.go b/agent/log/config.go new file mode 100644 index 000000000..08c1d6173 --- /dev/null +++ b/agent/log/config.go @@ -0,0 +1,41 @@ +package log + +import ( + "errors" + "io" + "os" + "path" +) + +var ( + BufferSize = 0x100000 + DefaultFileMode = os.FileMode(0644) + DefaultFileFlag = os.O_RDWR | os.O_CREATE | os.O_APPEND + ErrInvalidArgument = errors.New("error argument invalid") + QueueSize = 1024 + ErrClosed = errors.New("error write on close") +) + +type Config struct { + TimeTagFormat string + LogPath string + FileName string + LogSuffix string + MaxRemain int + RollingTimePattern string +} + +type Manager interface { + Fire() chan string + Close() +} + +type RollingWriter interface { + io.Writer + Close() error +} + +func FilePath(c *Config) (filepath string) { + filepath = path.Join(c.LogPath, c.FileName) + c.LogSuffix + return +} diff --git a/agent/log/dup_write_darwin.go b/agent/log/dup_write_darwin.go new file mode 100644 index 000000000..822d24e82 --- /dev/null +++ b/agent/log/dup_write_darwin.go @@ -0,0 +1,20 @@ +package log + +import ( + "golang.org/x/sys/unix" + "os" + "runtime" +) + +var stdErrFileHandler *os.File + +func dupWrite(file *os.File) error { + stdErrFileHandler = file + if err := unix.Dup2(int(file.Fd()), int(os.Stderr.Fd())); err != nil { + return err + } + runtime.SetFinalizer(stdErrFileHandler, func(fd *os.File) { + fd.Close() + }) + return nil +} diff --git a/agent/log/dup_write_linux.go b/agent/log/dup_write_linux.go new file mode 100644 index 000000000..822d24e82 --- /dev/null +++ b/agent/log/dup_write_linux.go @@ -0,0 +1,20 @@ +package log + +import ( + "golang.org/x/sys/unix" + "os" + "runtime" +) + +var stdErrFileHandler *os.File + +func dupWrite(file *os.File) error { + stdErrFileHandler = file + if err := unix.Dup2(int(file.Fd()), int(os.Stderr.Fd())); err != nil { + return err + } + runtime.SetFinalizer(stdErrFileHandler, func(fd *os.File) { + fd.Close() + }) + return nil +} diff --git a/agent/log/dup_write_windows.go b/agent/log/dup_write_windows.go new file mode 100644 index 000000000..4996cc12d --- /dev/null +++ b/agent/log/dup_write_windows.go @@ -0,0 +1,9 @@ +package log + +import ( + "os" +) + +func dupWrite(file *os.File) error { + return nil +} diff --git a/agent/log/manager.go b/agent/log/manager.go new file mode 100644 index 000000000..1f4f94546 --- /dev/null +++ b/agent/log/manager.go @@ -0,0 +1,53 @@ +package log + +import ( + "github.com/robfig/cron/v3" + "path" + "sync" + "time" +) + +type manager struct { + startAt time.Time + fire chan string + cr *cron.Cron + context chan int + wg sync.WaitGroup + lock sync.Mutex +} + +func (m *manager) Fire() chan string { + return m.fire +} + +func (m *manager) Close() { + close(m.context) + m.cr.Stop() +} + +func NewManager(c *Config) (Manager, error) { + m := &manager{ + startAt: time.Now(), + cr: cron.New(), + fire: make(chan string), + context: make(chan int), + wg: sync.WaitGroup{}, + } + + if _, err := m.cr.AddFunc(c.RollingTimePattern, func() { + m.fire <- m.GenLogFileName(c) + }); err != nil { + return nil, err + } + m.cr.Start() + + return m, nil +} + +func (m *manager) GenLogFileName(c *Config) (filename string) { + m.lock.Lock() + filename = path.Join(c.LogPath, c.FileName+"-"+m.startAt.Format(c.TimeTagFormat)) + c.LogSuffix + m.startAt = time.Now() + m.lock.Unlock() + return +} diff --git a/agent/log/writer.go b/agent/log/writer.go new file mode 100644 index 000000000..c74979f82 --- /dev/null +++ b/agent/log/writer.go @@ -0,0 +1,250 @@ +package log + +import ( + "log" + "os" + "path" + "sort" + "strings" + "sync" + "sync/atomic" + "time" + "unsafe" + + "github.com/1Panel-dev/1Panel/agent/global" +) + +type Writer struct { + m Manager + file *os.File + absPath string + fire chan string + cf *Config + rollingfilech chan string +} + +type AsynchronousWriter struct { + Writer + ctx chan int + queue chan []byte + errChan chan error + closed int32 + wg sync.WaitGroup +} + +func (w *AsynchronousWriter) Close() error { + if atomic.CompareAndSwapInt32(&w.closed, 0, 1) { + close(w.ctx) + w.onClose() + + func() { + defer func() { + if r := recover(); r != nil { + global.LOG.Error(r) + } + }() + w.m.Close() + }() + return w.file.Close() + } + return ErrClosed +} + +func (w *AsynchronousWriter) onClose() { + var err error + for { + select { + case b := <-w.queue: + if _, err = w.file.Write(b); err != nil { + select { + case w.errChan <- err: + default: + _asyncBufferPool.Put(&b) + return + } + } + _asyncBufferPool.Put(&b) + default: + return + } + } +} + +var _asyncBufferPool = sync.Pool{ + New: func() interface{} { + return make([]byte, BufferSize) + }, +} + +func NewWriterFromConfig(c *Config) (RollingWriter, error) { + if c.LogPath == "" || c.FileName == "" { + return nil, ErrInvalidArgument + } + if err := os.MkdirAll(c.LogPath, 0700); err != nil { + return nil, err + } + filepath := FilePath(c) + file, err := os.OpenFile(filepath, os.O_RDWR|os.O_CREATE|os.O_APPEND, 0644) + if err != nil { + return nil, err + } + if err := dupWrite(file); err != nil { + return nil, err + } + mng, err := NewManager(c) + if err != nil { + return nil, err + } + + var rollingWriter RollingWriter + writer := Writer{ + m: mng, + file: file, + absPath: filepath, + fire: mng.Fire(), + cf: c, + } + if c.MaxRemain > 0 { + writer.rollingfilech = make(chan string, c.MaxRemain) + dir, err := os.ReadDir(c.LogPath) + if err != nil { + mng.Close() + return nil, err + } + + files := make([]string, 0, 10) + for _, fi := range dir { + if fi.IsDir() { + continue + } + + fileName := c.FileName + if strings.Contains(fi.Name(), fileName) && strings.Contains(fi.Name(), c.LogSuffix) { + start := strings.Index(fi.Name(), "-") + end := strings.Index(fi.Name(), c.LogSuffix) + name := fi.Name() + if start > 0 && end > 0 { + _, err := time.Parse(c.TimeTagFormat, name[start+1:end]) + if err == nil { + files = append(files, fi.Name()) + } + } + } + } + sort.Slice(files, func(i, j int) bool { + t1Start := strings.Index(files[i], "-") + t1End := strings.Index(files[i], c.LogSuffix) + t2Start := strings.Index(files[i], "-") + t2End := strings.Index(files[i], c.LogSuffix) + t1, _ := time.Parse(c.TimeTagFormat, files[i][t1Start+1:t1End]) + t2, _ := time.Parse(c.TimeTagFormat, files[j][t2Start+1:t2End]) + return t1.Before(t2) + }) + + for _, file := range files { + retry: + select { + case writer.rollingfilech <- path.Join(c.LogPath, file): + default: + writer.DoRemove() + goto retry + } + } + } + + wr := &AsynchronousWriter{ + ctx: make(chan int), + queue: make(chan []byte, QueueSize), + errChan: make(chan error, QueueSize), + wg: sync.WaitGroup{}, + closed: 0, + Writer: writer, + } + + wr.wg.Add(1) + go wr.writer() + wr.wg.Wait() + rollingWriter = wr + + return rollingWriter, nil +} + +func (w *AsynchronousWriter) writer() { + var err error + w.wg.Done() + for { + select { + case filename := <-w.fire: + if err = w.Reopen(filename); err != nil && len(w.errChan) < cap(w.errChan) { + w.errChan <- err + } + case b := <-w.queue: + if _, err = w.file.Write(b); err != nil && len(w.errChan) < cap(w.errChan) { + w.errChan <- err + } + _asyncBufferPool.Put(&b) + case <-w.ctx: + return + } + } +} + +func (w *Writer) DoRemove() { + file := <-w.rollingfilech + if err := os.Remove(file); err != nil { + log.Println("error in remove log file", file, err) + } +} + +func (w *Writer) Write(b []byte) (int, error) { + var ok = false + for !ok { + select { + case filename := <-w.fire: + if err := w.Reopen(filename); err != nil { + return 0, err + } + default: + ok = true + } + } + + fp := atomic.LoadPointer((*unsafe.Pointer)(unsafe.Pointer(&w.file))) + file := (*os.File)(fp) + return file.Write(b) +} + +func (w *Writer) Reopen(file string) error { + fileInfo, err := w.file.Stat() + if err != nil { + return err + } + + if fileInfo.Size() == 0 { + return nil + } + + w.file.Close() + if err := os.Rename(w.absPath, file); err != nil { + return err + } + newFile, err := os.OpenFile(w.absPath, DefaultFileFlag, DefaultFileMode) + if err != nil { + return err + } + + w.file = newFile + + go func() { + if w.cf.MaxRemain > 0 { + retry: + select { + case w.rollingfilech <- file: + default: + w.DoRemove() + goto retry + } + } + }() + return nil +} diff --git a/agent/middleware/demo_handle.go b/agent/middleware/demo_handle.go new file mode 100644 index 000000000..1843406ba --- /dev/null +++ b/agent/middleware/demo_handle.go @@ -0,0 +1,57 @@ +package middleware + +import ( + "net/http" + "strings" + + "github.com/1Panel-dev/1Panel/agent/app/dto" + "github.com/1Panel-dev/1Panel/agent/buserr" + "github.com/1Panel-dev/1Panel/agent/constant" + "github.com/gin-gonic/gin" +) + +var whiteUrlList = map[string]struct{}{ + "/api/v1/auth/login": {}, + "/api/v1/websites/config": {}, + "/api/v1/websites/waf/config": {}, + "/api/v1/files/loadfile": {}, + "/api/v1/files/size": {}, + "/api/v1/logs/operation": {}, + "/api/v1/logs/login": {}, + "/api/v1/auth/logout": {}, + + "/api/v1/apps/installed/loadport": {}, + "/api/v1/apps/installed/check": {}, + "/api/v1/apps/installed/conninfo": {}, + "/api/v1/databases/load/file": {}, + "/api/v1/databases/variables": {}, + "/api/v1/databases/status": {}, + "/api/v1/databases/baseinfo": {}, + + "/api/v1/waf/attack/stat": {}, + "/api/v1/waf/config/website": {}, + + "/api/v1/monitor/stat": {}, + "/api/v1/monitor/visitors": {}, + "/api/v1/monitor/visitors/loc": {}, + "/api/v1/monitor/qps": {}, +} + +func DemoHandle() gin.HandlerFunc { + return func(c *gin.Context) { + if strings.Contains(c.Request.URL.Path, "search") || c.Request.Method == http.MethodGet { + c.Next() + return + } + if _, ok := whiteUrlList[c.Request.URL.Path]; ok { + c.Next() + return + } + + c.JSON(http.StatusInternalServerError, dto.Response{ + Code: http.StatusInternalServerError, + Message: buserr.New(constant.ErrDemoEnvironment).Error(), + }) + c.Abort() + } +} diff --git a/agent/middleware/helper.go b/agent/middleware/helper.go new file mode 100644 index 000000000..4a9a7d62c --- /dev/null +++ b/agent/middleware/helper.go @@ -0,0 +1,32 @@ +package middleware + +import ( + "net/http" + + "github.com/1Panel-dev/1Panel/agent/app/repo" +) + +func LoadErrCode(errInfo string) int { + settingRepo := repo.NewISettingRepo() + codeVal, err := settingRepo.Get(settingRepo.WithByKey("NoAuthSetting")) + if err != nil { + return 500 + } + + switch codeVal.Value { + case "400": + return http.StatusBadRequest + case "401": + return http.StatusUnauthorized + case "403": + return http.StatusForbidden + case "404": + return http.StatusNotFound + case "408": + return http.StatusRequestTimeout + case "416": + return http.StatusRequestedRangeNotSatisfiable + default: + return http.StatusOK + } +} diff --git a/agent/middleware/loading.go b/agent/middleware/loading.go new file mode 100644 index 000000000..f935e34d6 --- /dev/null +++ b/agent/middleware/loading.go @@ -0,0 +1,24 @@ +package middleware + +import ( + "github.com/1Panel-dev/1Panel/agent/app/api/v1/helper" + "github.com/1Panel-dev/1Panel/agent/app/repo" + "github.com/1Panel-dev/1Panel/agent/constant" + "github.com/gin-gonic/gin" +) + +func GlobalLoading() gin.HandlerFunc { + return func(c *gin.Context) { + settingRepo := repo.NewISettingRepo() + status, err := settingRepo.Get(settingRepo.WithByKey("SystemStatus")) + if err != nil { + helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) + return + } + if status.Value != "Free" { + helper.ErrorWithDetail(c, constant.CodeGlobalLoading, status.Value, err) + return + } + c.Next() + } +} diff --git a/agent/router/common.go b/agent/router/common.go new file mode 100644 index 000000000..b96153767 --- /dev/null +++ b/agent/router/common.go @@ -0,0 +1,26 @@ +package router + +func commonGroups() []CommonRouter { + return []CommonRouter{ + &DashboardRouter{}, + &HostRouter{}, + &ContainerRouter{}, + &LogRouter{}, + &FileRouter{}, + &ToolboxRouter{}, + &TerminalRouter{}, + &CronjobRouter{}, + &SettingRouter{}, + &AppRouter{}, + &WebsiteRouter{}, + &WebsiteGroupRouter{}, + &WebsiteDnsAccountRouter{}, + &WebsiteAcmeAccountRouter{}, + &WebsiteSSLRouter{}, + &DatabaseRouter{}, + &NginxRouter{}, + &RuntimeRouter{}, + &ProcessRouter{}, + &WebsiteCARouter{}, + } +} diff --git a/agent/router/entry.go b/agent/router/entry.go new file mode 100644 index 000000000..49ed25fd1 --- /dev/null +++ b/agent/router/entry.go @@ -0,0 +1,9 @@ +//go:build !xpack + +package router + +func RouterGroups() []CommonRouter { + return commonGroups() +} + +var RouterGroupApp = RouterGroups() diff --git a/agent/router/entry_xpack.go b/agent/router/entry_xpack.go new file mode 120000 index 000000000..4a98d3368 --- /dev/null +++ b/agent/router/entry_xpack.go @@ -0,0 +1 @@ +/Users/slooop/Documents/mycode/xpack-backend/other/entry_xpack.go \ No newline at end of file diff --git a/agent/router/ro_app.go b/agent/router/ro_app.go new file mode 100644 index 000000000..82cf8d02b --- /dev/null +++ b/agent/router/ro_app.go @@ -0,0 +1,41 @@ +package router + +import ( + v1 "github.com/1Panel-dev/1Panel/agent/app/api/v1" + "github.com/gin-gonic/gin" +) + +type AppRouter struct { +} + +func (a *AppRouter) InitRouter(Router *gin.RouterGroup) { + appRouter := Router.Group("apps") + + baseApi := v1.ApiGroupApp.BaseApi + { + appRouter.POST("/sync", baseApi.SyncApp) + appRouter.GET("/checkupdate", baseApi.GetAppListUpdate) + appRouter.POST("/search", baseApi.SearchApp) + appRouter.GET("/:key", baseApi.GetApp) + appRouter.GET("/detail/:appId/:version/:type", baseApi.GetAppDetail) + appRouter.GET("/details/:id", baseApi.GetAppDetailByID) + appRouter.POST("/install", baseApi.InstallApp) + appRouter.GET("/tags", baseApi.GetAppTags) + appRouter.POST("/installed/check", baseApi.CheckAppInstalled) + appRouter.POST("/installed/loadport", baseApi.LoadPort) + appRouter.POST("/installed/conninfo", baseApi.LoadConnInfo) + appRouter.GET("/installed/delete/check/:appInstallId", baseApi.DeleteCheck) + appRouter.POST("/installed/search", baseApi.SearchAppInstalled) + appRouter.GET("/installed/list", baseApi.ListAppInstalled) + appRouter.POST("/installed/op", baseApi.OperateInstalled) + appRouter.POST("/installed/sync", baseApi.SyncInstalled) + appRouter.POST("/installed/port/change", baseApi.ChangeAppPort) + appRouter.GET("/services/:key", baseApi.GetServices) + appRouter.POST("/installed/conf", baseApi.GetDefaultConfig) + appRouter.GET("/installed/params/:appInstallId", baseApi.GetParams) + appRouter.POST("/installed/params/update", baseApi.UpdateInstalled) + appRouter.POST("/installed/ignore", baseApi.IgnoreUpgrade) + appRouter.GET("/ignored/detail", baseApi.GetIgnoredApp) + appRouter.POST("/installed/update/versions", baseApi.GetUpdateVersions) + } +} diff --git a/agent/router/ro_container.go b/agent/router/ro_container.go new file mode 100644 index 000000000..683c8f8e6 --- /dev/null +++ b/agent/router/ro_container.go @@ -0,0 +1,84 @@ +package router + +import ( + v1 "github.com/1Panel-dev/1Panel/agent/app/api/v1" + "github.com/gin-gonic/gin" +) + +type ContainerRouter struct{} + +func (s *ContainerRouter) InitRouter(Router *gin.RouterGroup) { + baRouter := Router.Group("containers") + baseApi := v1.ApiGroupApp.BaseApi + { + baRouter.GET("/exec", baseApi.ContainerWsSsh) + baRouter.GET("/stats/:id", baseApi.ContainerStats) + + baRouter.POST("", baseApi.ContainerCreate) + baRouter.POST("/update", baseApi.ContainerUpdate) + baRouter.POST("/upgrade", baseApi.ContainerUpgrade) + baRouter.POST("/info", baseApi.ContainerInfo) + baRouter.POST("/search", baseApi.SearchContainer) + baRouter.POST("/list", baseApi.ListContainer) + baRouter.GET("/list/stats", baseApi.ContainerListStats) + baRouter.GET("/search/log", baseApi.ContainerLogs) + baRouter.POST("/download/log", baseApi.DownloadContainerLogs) + baRouter.GET("/limit", baseApi.LoadResourceLimit) + baRouter.POST("/clean/log", baseApi.CleanContainerLog) + baRouter.POST("/load/log", baseApi.LoadContainerLog) + baRouter.POST("/inspect", baseApi.Inspect) + baRouter.POST("/rename", baseApi.ContainerRename) + baRouter.POST("/commit", baseApi.ContainerCommit) + baRouter.POST("/operate", baseApi.ContainerOperation) + baRouter.POST("/prune", baseApi.ContainerPrune) + + baRouter.GET("/repo", baseApi.ListRepo) + baRouter.POST("/repo/status", baseApi.CheckRepoStatus) + baRouter.POST("/repo/search", baseApi.SearchRepo) + baRouter.POST("/repo/update", baseApi.UpdateRepo) + baRouter.POST("/repo", baseApi.CreateRepo) + baRouter.POST("/repo/del", baseApi.DeleteRepo) + + baRouter.POST("/compose/search", baseApi.SearchCompose) + baRouter.POST("/compose", baseApi.CreateCompose) + baRouter.POST("/compose/test", baseApi.TestCompose) + baRouter.POST("/compose/operate", baseApi.OperatorCompose) + baRouter.POST("/compose/update", baseApi.ComposeUpdate) + baRouter.GET("/compose/search/log", baseApi.ComposeLogs) + + baRouter.GET("/template", baseApi.ListComposeTemplate) + baRouter.POST("/template/search", baseApi.SearchComposeTemplate) + baRouter.POST("/template/update", baseApi.UpdateComposeTemplate) + baRouter.POST("/template", baseApi.CreateComposeTemplate) + baRouter.POST("/template/del", baseApi.DeleteComposeTemplate) + + baRouter.GET("/image", baseApi.ListImage) + baRouter.GET("/image/all", baseApi.ListAllImage) + baRouter.POST("/image/search", baseApi.SearchImage) + baRouter.POST("/image/pull", baseApi.ImagePull) + baRouter.POST("/image/push", baseApi.ImagePush) + baRouter.POST("/image/save", baseApi.ImageSave) + baRouter.POST("/image/load", baseApi.ImageLoad) + baRouter.POST("/image/remove", baseApi.ImageRemove) + baRouter.POST("/image/tag", baseApi.ImageTag) + baRouter.POST("/image/build", baseApi.ImageBuild) + + baRouter.GET("/network", baseApi.ListNetwork) + baRouter.POST("/network/del", baseApi.DeleteNetwork) + baRouter.POST("/network/search", baseApi.SearchNetwork) + baRouter.POST("/network", baseApi.CreateNetwork) + baRouter.GET("/volume", baseApi.ListVolume) + baRouter.POST("/volume/del", baseApi.DeleteVolume) + baRouter.POST("/volume/search", baseApi.SearchVolume) + baRouter.POST("/volume", baseApi.CreateVolume) + + baRouter.GET("/daemonjson", baseApi.LoadDaemonJson) + baRouter.GET("/daemonjson/file", baseApi.LoadDaemonJsonFile) + baRouter.GET("/docker/status", baseApi.LoadDockerStatus) + baRouter.POST("/docker/operate", baseApi.OperateDocker) + baRouter.POST("/daemonjson/update", baseApi.UpdateDaemonJson) + baRouter.POST("/logoption/update", baseApi.UpdateLogOption) + baRouter.POST("/ipv6option/update", baseApi.UpdateIpv6Option) + baRouter.POST("/daemonjson/update/byfile", baseApi.UpdateDaemonJsonByFile) + } +} diff --git a/agent/router/ro_cronjob.go b/agent/router/ro_cronjob.go new file mode 100644 index 000000000..fc4a6a780 --- /dev/null +++ b/agent/router/ro_cronjob.go @@ -0,0 +1,26 @@ +package router + +import ( + v1 "github.com/1Panel-dev/1Panel/agent/app/api/v1" + + "github.com/gin-gonic/gin" +) + +type CronjobRouter struct{} + +func (s *CronjobRouter) InitRouter(Router *gin.RouterGroup) { + cmdRouter := Router.Group("cronjobs") + baseApi := v1.ApiGroupApp.BaseApi + { + cmdRouter.POST("", baseApi.CreateCronjob) + cmdRouter.POST("/del", baseApi.DeleteCronjob) + cmdRouter.POST("/update", baseApi.UpdateCronjob) + cmdRouter.POST("/status", baseApi.UpdateCronjobStatus) + cmdRouter.POST("/handle", baseApi.HandleOnce) + cmdRouter.POST("/download", baseApi.TargetDownload) + cmdRouter.POST("/search", baseApi.SearchCronjob) + cmdRouter.POST("/search/records", baseApi.SearchJobRecords) + cmdRouter.POST("/records/log", baseApi.LoadRecordLog) + cmdRouter.POST("/records/clean", baseApi.CleanRecord) + } +} diff --git a/agent/router/ro_dashboard.go b/agent/router/ro_dashboard.go new file mode 100644 index 000000000..0dded9250 --- /dev/null +++ b/agent/router/ro_dashboard.go @@ -0,0 +1,20 @@ +package router + +import ( + v1 "github.com/1Panel-dev/1Panel/agent/app/api/v1" + + "github.com/gin-gonic/gin" +) + +type DashboardRouter struct{} + +func (s *DashboardRouter) InitRouter(Router *gin.RouterGroup) { + cmdRouter := Router.Group("dashboard") + 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) + } +} diff --git a/agent/router/ro_database.go b/agent/router/ro_database.go new file mode 100644 index 000000000..0bb99b1eb --- /dev/null +++ b/agent/router/ro_database.go @@ -0,0 +1,64 @@ +package router + +import ( + v1 "github.com/1Panel-dev/1Panel/agent/app/api/v1" + + "github.com/gin-gonic/gin" +) + +type DatabaseRouter struct{} + +func (s *DatabaseRouter) InitRouter(Router *gin.RouterGroup) { + cmdRouter := Router.Group("databases") + baseApi := v1.ApiGroupApp.BaseApi + { + cmdRouter.POST("/common/info", baseApi.LoadDBBaseInfo) + cmdRouter.POST("/common/load/file", baseApi.LoadDBFile) + cmdRouter.POST("/common/update/conf", baseApi.UpdateDBConfByFile) + + cmdRouter.POST("", baseApi.CreateMysql) + cmdRouter.POST("/bind", baseApi.BindUser) + cmdRouter.POST("load", baseApi.LoadDBFromRemote) + cmdRouter.POST("/change/access", baseApi.ChangeMysqlAccess) + cmdRouter.POST("/change/password", baseApi.ChangeMysqlPassword) + cmdRouter.POST("/del/check", baseApi.DeleteCheckMysql) + cmdRouter.POST("/del", baseApi.DeleteMysql) + cmdRouter.POST("/description/update", baseApi.UpdateMysqlDescription) + cmdRouter.POST("/variables/update", baseApi.UpdateMysqlVariables) + cmdRouter.POST("/search", baseApi.SearchMysql) + cmdRouter.POST("/variables", baseApi.LoadVariables) + cmdRouter.POST("/status", baseApi.LoadStatus) + cmdRouter.POST("/remote", baseApi.LoadRemoteAccess) + cmdRouter.GET("/options", baseApi.ListDBName) + + cmdRouter.POST("/redis/persistence/conf", baseApi.LoadPersistenceConf) + cmdRouter.POST("/redis/status", baseApi.LoadRedisStatus) + cmdRouter.POST("/redis/conf", baseApi.LoadRedisConf) + cmdRouter.GET("/redis/exec", baseApi.RedisWsSsh) + cmdRouter.GET("/redis/check", baseApi.CheckHasCli) + cmdRouter.POST("/redis/install/cli", baseApi.InstallCli) + cmdRouter.POST("/redis/password", baseApi.ChangeRedisPassword) + cmdRouter.POST("/redis/conf/update", baseApi.UpdateRedisConf) + cmdRouter.POST("/redis/persistence/update", baseApi.UpdateRedisPersistenceConf) + + cmdRouter.POST("/db/check", baseApi.CheckDatabase) + cmdRouter.POST("/db", baseApi.CreateDatabase) + cmdRouter.GET("/db/:name", baseApi.GetDatabase) + cmdRouter.GET("/db/list/:type", baseApi.ListDatabase) + cmdRouter.GET("/db/item/:type", baseApi.LoadDatabaseItems) + cmdRouter.POST("/db/update", baseApi.UpdateDatabase) + cmdRouter.POST("/db/search", baseApi.SearchDatabase) + cmdRouter.POST("/db/del/check", baseApi.DeleteCheckDatabase) + cmdRouter.POST("/db/del", baseApi.DeleteDatabase) + + cmdRouter.POST("/pg", baseApi.CreatePostgresql) + cmdRouter.POST("/pg/search", baseApi.SearchPostgresql) + cmdRouter.POST("/pg/:database/load", baseApi.LoadPostgresqlDBFromRemote) + cmdRouter.POST("/pg/bind", baseApi.BindPostgresqlUser) + cmdRouter.POST("/pg/del/check", baseApi.DeleteCheckPostgresql) + cmdRouter.POST("/pg/del", baseApi.DeletePostgresql) + cmdRouter.POST("/pg/privileges", baseApi.ChangePostgresqlPrivileges) + cmdRouter.POST("/pg/password", baseApi.ChangePostgresqlPassword) + cmdRouter.POST("/pg/description", baseApi.UpdatePostgresqlDescription) + } +} diff --git a/agent/router/ro_file.go b/agent/router/ro_file.go new file mode 100644 index 000000000..68d09a1e9 --- /dev/null +++ b/agent/router/ro_file.go @@ -0,0 +1,51 @@ +package router + +import ( + v1 "github.com/1Panel-dev/1Panel/agent/app/api/v1" + "github.com/gin-gonic/gin" +) + +type FileRouter struct { +} + +func (f *FileRouter) InitRouter(Router *gin.RouterGroup) { + fileRouter := Router.Group("files") + baseApi := v1.ApiGroupApp.BaseApi + { + fileRouter.POST("/search", baseApi.ListFiles) + fileRouter.POST("/upload/search", baseApi.SearchUploadWithPage) + fileRouter.POST("/tree", baseApi.GetFileTree) + fileRouter.POST("", baseApi.CreateFile) + fileRouter.POST("/del", baseApi.DeleteFile) + fileRouter.POST("/batch/del", baseApi.BatchDeleteFile) + fileRouter.POST("/mode", baseApi.ChangeFileMode) + fileRouter.POST("/owner", baseApi.ChangeFileOwner) + fileRouter.POST("/compress", baseApi.CompressFile) + fileRouter.POST("/decompress", baseApi.DeCompressFile) + fileRouter.POST("/content", baseApi.GetContent) + fileRouter.POST("/save", baseApi.SaveContent) + fileRouter.POST("/check", baseApi.CheckFile) + fileRouter.POST("/upload", baseApi.UploadFiles) + fileRouter.POST("/chunkupload", baseApi.UploadChunkFiles) + fileRouter.POST("/rename", baseApi.ChangeFileName) + fileRouter.POST("/wget", baseApi.WgetFile) + fileRouter.POST("/move", baseApi.MoveFile) + fileRouter.GET("/download", baseApi.Download) + fileRouter.POST("/chunkdownload", baseApi.DownloadChunkFiles) + fileRouter.POST("/size", baseApi.Size) + fileRouter.GET("/ws", baseApi.Ws) + fileRouter.GET("/keys", baseApi.Keys) + fileRouter.POST("/read", baseApi.ReadFileByLine) + fileRouter.POST("/batch/role", baseApi.BatchChangeModeAndOwner) + + fileRouter.POST("/recycle/search", baseApi.SearchRecycleBinFile) + fileRouter.POST("/recycle/reduce", baseApi.ReduceRecycleBinFile) + fileRouter.POST("/recycle/clear", baseApi.ClearRecycleBinFile) + fileRouter.GET("/recycle/status", baseApi.GetRecycleStatus) + + fileRouter.POST("/favorite/search", baseApi.SearchFavorite) + fileRouter.POST("/favorite", baseApi.CreateFavorite) + fileRouter.POST("/favorite/del", baseApi.DeleteFavorite) + + } +} diff --git a/agent/router/ro_group.go b/agent/router/ro_group.go new file mode 100644 index 000000000..8e263dbe1 --- /dev/null +++ b/agent/router/ro_group.go @@ -0,0 +1,21 @@ +package router + +import ( + v1 "github.com/1Panel-dev/1Panel/agent/app/api/v1" + "github.com/gin-gonic/gin" +) + +type WebsiteGroupRouter struct { +} + +func (a *WebsiteGroupRouter) InitRouter(Router *gin.RouterGroup) { + groupRouter := Router.Group("groups") + + baseApi := v1.ApiGroupApp.BaseApi + { + groupRouter.POST("", baseApi.CreateGroup) + groupRouter.POST("/del", baseApi.DeleteGroup) + groupRouter.POST("/update", baseApi.UpdateGroup) + groupRouter.POST("/search", baseApi.ListGroup) + } +} diff --git a/agent/router/ro_host.go b/agent/router/ro_host.go new file mode 100644 index 000000000..e90e15254 --- /dev/null +++ b/agent/router/ro_host.go @@ -0,0 +1,70 @@ +package router + +import ( + v1 "github.com/1Panel-dev/1Panel/agent/app/api/v1" + + "github.com/gin-gonic/gin" +) + +type HostRouter struct{} + +func (s *HostRouter) InitRouter(Router *gin.RouterGroup) { + hostRouter := Router.Group("hosts") + baseApi := v1.ApiGroupApp.BaseApi + { + hostRouter.POST("", baseApi.CreateHost) + hostRouter.POST("/del", baseApi.DeleteHost) + hostRouter.POST("/update", baseApi.UpdateHost) + hostRouter.POST("/update/group", baseApi.UpdateHostGroup) + hostRouter.POST("/search", baseApi.SearchHost) + hostRouter.POST("/tree", baseApi.HostTree) + hostRouter.POST("/test/byinfo", baseApi.TestByInfo) + hostRouter.POST("/test/byid/:id", baseApi.TestByID) + + hostRouter.GET("/firewall/base", baseApi.LoadFirewallBaseInfo) + hostRouter.POST("/firewall/search", baseApi.SearchFirewallRule) + hostRouter.POST("/firewall/operate", baseApi.OperateFirewall) + hostRouter.POST("/firewall/port", baseApi.OperatePortRule) + hostRouter.POST("/firewall/forward", baseApi.OperateForwardRule) + hostRouter.POST("/firewall/ip", baseApi.OperateIPRule) + hostRouter.POST("/firewall/batch", baseApi.BatchOperateRule) + hostRouter.POST("/firewall/update/port", baseApi.UpdatePortRule) + hostRouter.POST("/firewall/update/addr", baseApi.UpdateAddrRule) + hostRouter.POST("/firewall/update/description", baseApi.UpdateFirewallDescription) + + hostRouter.POST("/monitor/search", baseApi.LoadMonitor) + hostRouter.POST("/monitor/clean", baseApi.CleanMonitor) + hostRouter.GET("/monitor/netoptions", baseApi.GetNetworkOptions) + hostRouter.GET("/monitor/iooptions", baseApi.GetIOOptions) + + hostRouter.GET("/ssh/conf", baseApi.LoadSSHConf) + hostRouter.POST("/ssh/search", baseApi.GetSSHInfo) + hostRouter.POST("/ssh/update", baseApi.UpdateSSH) + hostRouter.POST("/ssh/generate", baseApi.GenerateSSH) + hostRouter.POST("/ssh/secret", baseApi.LoadSSHSecret) + hostRouter.POST("/ssh/log", baseApi.LoadSSHLogs) + hostRouter.POST("/ssh/conffile/update", baseApi.UpdateSSHByfile) + hostRouter.POST("/ssh/operate", baseApi.OperateSSH) + + hostRouter.GET("/command", baseApi.ListCommand) + hostRouter.POST("/command", baseApi.CreateCommand) + hostRouter.POST("/command/del", baseApi.DeleteCommand) + hostRouter.POST("/command/search", baseApi.SearchCommand) + hostRouter.GET("/command/tree", baseApi.SearchCommandTree) + hostRouter.POST("/command/update", baseApi.UpdateCommand) + + hostRouter.GET("/command/redis", baseApi.ListRedisCommand) + hostRouter.POST("/command/redis", baseApi.SaveRedisCommand) + hostRouter.POST("/command/redis/search", baseApi.SearchRedisCommand) + hostRouter.POST("/command/redis/del", baseApi.DeleteRedisCommand) + + hostRouter.POST("/tool", baseApi.GetToolStatus) + hostRouter.POST("/tool/init", baseApi.InitToolConfig) + hostRouter.POST("/tool/operate", baseApi.OperateTool) + hostRouter.POST("/tool/config", baseApi.OperateToolConfig) + hostRouter.POST("/tool/log", baseApi.GetToolLog) + hostRouter.POST("/tool/supervisor/process", baseApi.OperateProcess) + hostRouter.GET("/tool/supervisor/process", baseApi.GetProcess) + hostRouter.POST("/tool/supervisor/process/file", baseApi.GetProcessFile) + } +} diff --git a/agent/router/ro_log.go b/agent/router/ro_log.go new file mode 100644 index 000000000..8b60ebf56 --- /dev/null +++ b/agent/router/ro_log.go @@ -0,0 +1,18 @@ +package router + +import ( + v1 "github.com/1Panel-dev/1Panel/agent/app/api/v1" + + "github.com/gin-gonic/gin" +) + +type LogRouter struct{} + +func (s *LogRouter) InitRouter(Router *gin.RouterGroup) { + operationRouter := Router.Group("logs") + baseApi := v1.ApiGroupApp.BaseApi + { + operationRouter.GET("/system/files", baseApi.GetSystemFiles) + operationRouter.POST("/system", baseApi.GetSystemLogs) + } +} diff --git a/agent/router/ro_nginx.go b/agent/router/ro_nginx.go new file mode 100644 index 000000000..aa57eac5e --- /dev/null +++ b/agent/router/ro_nginx.go @@ -0,0 +1,23 @@ +package router + +import ( + v1 "github.com/1Panel-dev/1Panel/agent/app/api/v1" + "github.com/gin-gonic/gin" +) + +type NginxRouter struct { +} + +func (a *NginxRouter) InitRouter(Router *gin.RouterGroup) { + groupRouter := Router.Group("openresty") + + baseApi := v1.ApiGroupApp.BaseApi + { + groupRouter.GET("", baseApi.GetNginx) + groupRouter.POST("/scope", baseApi.GetNginxConfigByScope) + groupRouter.POST("/update", baseApi.UpdateNginxConfigByScope) + groupRouter.GET("/status", baseApi.GetNginxStatus) + groupRouter.POST("/file", baseApi.UpdateNginxFile) + groupRouter.POST("/clear", baseApi.ClearNginxProxyCache) + } +} diff --git a/agent/router/ro_process.go b/agent/router/ro_process.go new file mode 100644 index 000000000..677ce1829 --- /dev/null +++ b/agent/router/ro_process.go @@ -0,0 +1,18 @@ +package router + +import ( + v1 "github.com/1Panel-dev/1Panel/agent/app/api/v1" + "github.com/gin-gonic/gin" +) + +type ProcessRouter struct { +} + +func (f *ProcessRouter) InitRouter(Router *gin.RouterGroup) { + processRouter := Router.Group("process") + baseApi := v1.ApiGroupApp.BaseApi + { + processRouter.GET("/ws", baseApi.ProcessWs) + processRouter.POST("/stop", baseApi.StopProcess) + } +} diff --git a/agent/router/ro_router.go b/agent/router/ro_router.go new file mode 100644 index 000000000..58a52dce0 --- /dev/null +++ b/agent/router/ro_router.go @@ -0,0 +1,7 @@ +package router + +import "github.com/gin-gonic/gin" + +type CommonRouter interface { + InitRouter(Router *gin.RouterGroup) +} diff --git a/agent/router/ro_runtime.go b/agent/router/ro_runtime.go new file mode 100644 index 000000000..8bda07e57 --- /dev/null +++ b/agent/router/ro_runtime.go @@ -0,0 +1,35 @@ +package router + +import ( + v1 "github.com/1Panel-dev/1Panel/agent/app/api/v1" + "github.com/gin-gonic/gin" +) + +type RuntimeRouter struct { +} + +func (r *RuntimeRouter) InitRouter(Router *gin.RouterGroup) { + groupRouter := Router.Group("runtimes") + + baseApi := v1.ApiGroupApp.BaseApi + { + groupRouter.GET("/installed/delete/check/:runTimeId", baseApi.DeleteRuntimeCheck) + groupRouter.POST("/search", baseApi.SearchRuntimes) + groupRouter.POST("", baseApi.CreateRuntime) + 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) + groupRouter.POST("/node/modules", baseApi.GetNodeModules) + groupRouter.POST("/node/modules/operate", baseApi.OperateNodeModules) + + groupRouter.POST("/php/extensions/search", baseApi.PagePHPExtensions) + groupRouter.POST("/php/extensions", baseApi.CreatePHPExtensions) + groupRouter.POST("/php/extensions/update", baseApi.UpdatePHPExtensions) + groupRouter.POST("/php/extensions/del", baseApi.DeletePHPExtensions) + } + +} diff --git a/agent/router/ro_setting.go b/agent/router/ro_setting.go new file mode 100644 index 000000000..823124ec5 --- /dev/null +++ b/agent/router/ro_setting.go @@ -0,0 +1,45 @@ +package router + +import ( + v1 "github.com/1Panel-dev/1Panel/agent/app/api/v1" + "github.com/gin-gonic/gin" +) + +type SettingRouter struct{} + +func (s *SettingRouter) InitRouter(Router *gin.RouterGroup) { + settingRouter := Router.Group("settings") + baseApi := v1.ApiGroupApp.BaseApi + { + settingRouter.POST("/search", baseApi.GetSettingInfo) + settingRouter.GET("/search/available", baseApi.GetSystemAvailable) + settingRouter.POST("/update", baseApi.UpdateSetting) + + settingRouter.POST("/snapshot", baseApi.CreateSnapshot) + settingRouter.POST("/snapshot/status", baseApi.LoadSnapShotStatus) + settingRouter.POST("/snapshot/search", baseApi.SearchSnapshot) + settingRouter.POST("/snapshot/import", baseApi.ImportSnapshot) + settingRouter.POST("/snapshot/del", baseApi.DeleteSnapshot) + settingRouter.POST("/snapshot/recover", baseApi.RecoverSnapshot) + settingRouter.POST("/snapshot/rollback", baseApi.RollbackSnapshot) + settingRouter.POST("/snapshot/description/update", baseApi.UpdateSnapDescription) + + settingRouter.GET("/backup/search", baseApi.ListBackup) + settingRouter.GET("/backup/onedrive", baseApi.LoadOneDriveInfo) + settingRouter.POST("/backup/backup", baseApi.Backup) + settingRouter.POST("/backup/refresh/onedrive", baseApi.RefreshOneDriveToken) + settingRouter.POST("/backup/recover", baseApi.Recover) + settingRouter.POST("/backup/recover/byupload", baseApi.RecoverByUpload) + settingRouter.POST("/backup/search/files", baseApi.LoadFilesFromBackup) + settingRouter.POST("/backup/buckets", baseApi.ListBuckets) + settingRouter.POST("/backup", baseApi.CreateBackup) + settingRouter.POST("/backup/del", baseApi.DeleteBackup) + settingRouter.POST("/backup/update", baseApi.UpdateBackup) + settingRouter.POST("/backup/record/search", baseApi.SearchBackupRecords) + settingRouter.POST("/backup/record/search/bycronjob", baseApi.SearchBackupRecordsByCronjob) + settingRouter.POST("/backup/record/download", baseApi.DownloadRecord) + settingRouter.POST("/backup/record/del", baseApi.DeleteBackupRecord) + + settingRouter.GET("/basedir", baseApi.LoadBaseDir) + } +} diff --git a/agent/router/ro_terminal.go b/agent/router/ro_terminal.go new file mode 100644 index 000000000..5ddcd9201 --- /dev/null +++ b/agent/router/ro_terminal.go @@ -0,0 +1,17 @@ +package router + +import ( + v1 "github.com/1Panel-dev/1Panel/agent/app/api/v1" + + "github.com/gin-gonic/gin" +) + +type TerminalRouter struct{} + +func (s *TerminalRouter) InitRouter(Router *gin.RouterGroup) { + terminalRouter := Router.Group("terminals") + baseApi := v1.ApiGroupApp.BaseApi + { + terminalRouter.GET("", baseApi.WsSsh) + } +} diff --git a/agent/router/ro_toolbox.go b/agent/router/ro_toolbox.go new file mode 100644 index 000000000..bff40ad99 --- /dev/null +++ b/agent/router/ro_toolbox.go @@ -0,0 +1,59 @@ +package router + +import ( + v1 "github.com/1Panel-dev/1Panel/agent/app/api/v1" + + "github.com/gin-gonic/gin" +) + +type ToolboxRouter struct{} + +func (s *ToolboxRouter) InitRouter(Router *gin.RouterGroup) { + toolboxRouter := Router.Group("toolbox") + baseApi := v1.ApiGroupApp.BaseApi + { + toolboxRouter.POST("/device/base", baseApi.LoadDeviceBaseInfo) + toolboxRouter.GET("/device/zone/options", baseApi.LoadTimeOption) + toolboxRouter.POST("/device/update/conf", baseApi.UpdateDeviceConf) + toolboxRouter.POST("/device/update/host", baseApi.UpdateDeviceHost) + toolboxRouter.POST("/device/update/passwd", baseApi.UpdateDevicePasswd) + toolboxRouter.POST("/device/update/swap", baseApi.UpdateDeviceSwap) + toolboxRouter.POST("/device/update/byconf", baseApi.UpdateDeviceByFile) + toolboxRouter.POST("/device/check/dns", baseApi.CheckDNS) + toolboxRouter.POST("/device/conf", baseApi.LoadDeviceConf) + + toolboxRouter.POST("/scan", baseApi.ScanSystem) + toolboxRouter.POST("/clean", baseApi.SystemClean) + + toolboxRouter.GET("/fail2ban/base", baseApi.LoadFail2BanBaseInfo) + toolboxRouter.GET("/fail2ban/load/conf", baseApi.LoadFail2BanConf) + toolboxRouter.POST("/fail2ban/search", baseApi.SearchFail2Ban) + toolboxRouter.POST("/fail2ban/operate", baseApi.OperateFail2Ban) + toolboxRouter.POST("/fail2ban/operate/sshd", baseApi.OperateSSHD) + toolboxRouter.POST("/fail2ban/update", baseApi.UpdateFail2BanConf) + toolboxRouter.POST("/fail2ban/update/byconf", baseApi.UpdateFail2BanConfByFile) + + toolboxRouter.GET("/ftp/base", baseApi.LoadFtpBaseInfo) + toolboxRouter.POST("/ftp/log/search", baseApi.LoadFtpLogInfo) + toolboxRouter.POST("/ftp/operate", baseApi.OperateFtp) + toolboxRouter.POST("/ftp/search", baseApi.SearchFtp) + toolboxRouter.POST("/ftp", baseApi.CreateFtp) + toolboxRouter.POST("/ftp/update", baseApi.UpdateFtp) + toolboxRouter.POST("/ftp/del", baseApi.DeleteFtp) + toolboxRouter.POST("/ftp/sync", baseApi.SyncFtp) + + toolboxRouter.POST("/clam/search", baseApi.SearchClam) + toolboxRouter.POST("/clam/record/search", baseApi.SearchClamRecord) + toolboxRouter.POST("/clam/record/clean", baseApi.CleanClamRecord) + toolboxRouter.POST("/clam/record/log", baseApi.LoadClamRecordLog) + toolboxRouter.POST("/clam/file/search", baseApi.SearchClamFile) + toolboxRouter.POST("/clam/file/update", baseApi.UpdateFile) + toolboxRouter.POST("/clam", baseApi.CreateClam) + toolboxRouter.POST("/clam/base", baseApi.LoadClamBaseInfo) + toolboxRouter.POST("/clam/operate", baseApi.OperateClam) + toolboxRouter.POST("/clam/update", baseApi.UpdateClam) + toolboxRouter.POST("/clam/status/update", baseApi.UpdateClamStatus) + toolboxRouter.POST("/clam/del", baseApi.DeleteClam) + toolboxRouter.POST("/clam/handle", baseApi.HandleClamScan) + } +} diff --git a/agent/router/ro_website.go b/agent/router/ro_website.go new file mode 100644 index 000000000..ddb336c07 --- /dev/null +++ b/agent/router/ro_website.go @@ -0,0 +1,69 @@ +package router + +import ( + v1 "github.com/1Panel-dev/1Panel/agent/app/api/v1" + "github.com/gin-gonic/gin" +) + +type WebsiteRouter struct { +} + +func (a *WebsiteRouter) InitRouter(Router *gin.RouterGroup) { + websiteRouter := Router.Group("websites") + + baseApi := v1.ApiGroupApp.BaseApi + { + websiteRouter.POST("/search", baseApi.PageWebsite) + websiteRouter.GET("/list", baseApi.GetWebsites) + websiteRouter.POST("", baseApi.CreateWebsite) + websiteRouter.POST("/operate", baseApi.OpWebsite) + websiteRouter.POST("/log", baseApi.OpWebsiteLog) + websiteRouter.POST("/check", baseApi.CreateWebsiteCheck) + websiteRouter.GET("/options", baseApi.GetWebsiteOptions) + websiteRouter.POST("/update", baseApi.UpdateWebsite) + websiteRouter.GET("/:id", baseApi.GetWebsite) + websiteRouter.POST("/del", baseApi.DeleteWebsite) + websiteRouter.POST("/default/server", baseApi.ChangeDefaultServer) + + websiteRouter.GET("/domains/:websiteId", baseApi.GetWebDomains) + websiteRouter.POST("/domains/del", baseApi.DeleteWebDomain) + websiteRouter.POST("/domains", baseApi.CreateWebDomain) + + websiteRouter.GET("/:id/config/:type", baseApi.GetWebsiteNginx) + websiteRouter.POST("/config", baseApi.GetNginxConfig) + websiteRouter.POST("/config/update", baseApi.UpdateNginxConfig) + websiteRouter.POST("/nginx/update", baseApi.UpdateWebsiteNginxConfig) + + websiteRouter.GET("/:id/https", baseApi.GetHTTPSConfig) + websiteRouter.POST("/:id/https", baseApi.UpdateHTTPSConfig) + + websiteRouter.GET("/php/config/:id", baseApi.GetWebsitePHPConfig) + websiteRouter.POST("/php/config", baseApi.UpdateWebsitePHPConfig) + websiteRouter.POST("/php/update", baseApi.UpdatePHPFile) + websiteRouter.POST("/php/version", baseApi.ChangePHPVersion) + + websiteRouter.POST("/rewrite", baseApi.GetRewriteConfig) + websiteRouter.POST("/rewrite/update", baseApi.UpdateRewriteConfig) + + websiteRouter.POST("/dir/update", baseApi.UpdateSiteDir) + websiteRouter.POST("/dir/permission", baseApi.UpdateSiteDirPermission) + websiteRouter.POST("/dir", baseApi.GetDirConfig) + + websiteRouter.POST("/proxies", baseApi.GetProxyConfig) + websiteRouter.POST("/proxies/update", baseApi.UpdateProxyConfig) + websiteRouter.POST("/proxies/file", baseApi.UpdateProxyConfigFile) + + websiteRouter.POST("/auths", baseApi.GetAuthConfig) + websiteRouter.POST("/auths/update", baseApi.UpdateAuthConfig) + + websiteRouter.POST("/leech", baseApi.GetAntiLeech) + websiteRouter.POST("/leech/update", baseApi.UpdateAntiLeech) + + websiteRouter.POST("/redirect/update", baseApi.UpdateRedirectConfig) + websiteRouter.POST("/redirect", baseApi.GetRedirectConfig) + websiteRouter.POST("/redirect/file", baseApi.UpdateRedirectConfigFile) + + websiteRouter.GET("/default/html/:type", baseApi.GetDefaultHtml) + websiteRouter.POST("/default/html/update", baseApi.UpdateDefaultHtml) + } +} diff --git a/agent/router/ro_website_acme_account.go b/agent/router/ro_website_acme_account.go new file mode 100644 index 000000000..9a973cdb6 --- /dev/null +++ b/agent/router/ro_website_acme_account.go @@ -0,0 +1,20 @@ +package router + +import ( + v1 "github.com/1Panel-dev/1Panel/agent/app/api/v1" + "github.com/gin-gonic/gin" +) + +type WebsiteAcmeAccountRouter struct { +} + +func (a *WebsiteAcmeAccountRouter) InitRouter(Router *gin.RouterGroup) { + groupRouter := Router.Group("websites/acme") + + baseApi := v1.ApiGroupApp.BaseApi + { + groupRouter.POST("/search", baseApi.PageWebsiteAcmeAccount) + groupRouter.POST("", baseApi.CreateWebsiteAcmeAccount) + groupRouter.POST("/del", baseApi.DeleteWebsiteAcmeAccount) + } +} diff --git a/agent/router/ro_website_ca.go b/agent/router/ro_website_ca.go new file mode 100644 index 000000000..87380e796 --- /dev/null +++ b/agent/router/ro_website_ca.go @@ -0,0 +1,24 @@ +package router + +import ( + v1 "github.com/1Panel-dev/1Panel/agent/app/api/v1" + "github.com/gin-gonic/gin" +) + +type WebsiteCARouter struct { +} + +func (a *WebsiteCARouter) InitRouter(Router *gin.RouterGroup) { + groupRouter := Router.Group("websites/ca") + + baseApi := v1.ApiGroupApp.BaseApi + { + groupRouter.POST("/search", baseApi.PageWebsiteCA) + groupRouter.POST("", baseApi.CreateWebsiteCA) + groupRouter.POST("/del", baseApi.DeleteWebsiteCA) + groupRouter.POST("/obtain", baseApi.ObtainWebsiteCA) + groupRouter.POST("/renew", baseApi.RenewWebsiteCA) + groupRouter.GET("/:id", baseApi.GetWebsiteCA) + groupRouter.POST("/download", baseApi.DownloadCAFile) + } +} diff --git a/agent/router/ro_website_dns_account.go b/agent/router/ro_website_dns_account.go new file mode 100644 index 000000000..2498883d0 --- /dev/null +++ b/agent/router/ro_website_dns_account.go @@ -0,0 +1,21 @@ +package router + +import ( + v1 "github.com/1Panel-dev/1Panel/agent/app/api/v1" + "github.com/gin-gonic/gin" +) + +type WebsiteDnsAccountRouter struct { +} + +func (a *WebsiteDnsAccountRouter) InitRouter(Router *gin.RouterGroup) { + groupRouter := Router.Group("websites/dns") + + baseApi := v1.ApiGroupApp.BaseApi + { + groupRouter.POST("/search", baseApi.PageWebsiteDnsAccount) + groupRouter.POST("", baseApi.CreateWebsiteDnsAccount) + groupRouter.POST("/update", baseApi.UpdateWebsiteDnsAccount) + groupRouter.POST("/del", baseApi.DeleteWebsiteDnsAccount) + } +} diff --git a/agent/router/ro_website_ssl.go b/agent/router/ro_website_ssl.go new file mode 100644 index 000000000..cba76f7b3 --- /dev/null +++ b/agent/router/ro_website_ssl.go @@ -0,0 +1,27 @@ +package router + +import ( + v1 "github.com/1Panel-dev/1Panel/agent/app/api/v1" + "github.com/gin-gonic/gin" +) + +type WebsiteSSLRouter struct { +} + +func (a *WebsiteSSLRouter) InitRouter(Router *gin.RouterGroup) { + groupRouter := Router.Group("websites/ssl") + + baseApi := v1.ApiGroupApp.BaseApi + { + groupRouter.POST("/search", baseApi.PageWebsiteSSL) + groupRouter.POST("", baseApi.CreateWebsiteSSL) + groupRouter.POST("/resolve", baseApi.GetDNSResolve) + groupRouter.POST("/del", baseApi.DeleteWebsiteSSL) + groupRouter.GET("/website/:websiteId", baseApi.GetWebsiteSSLByWebsiteId) + groupRouter.GET("/:id", baseApi.GetWebsiteSSLById) + groupRouter.POST("/update", baseApi.UpdateWebsiteSSL) + groupRouter.POST("/upload", baseApi.UploadWebsiteSSL) + groupRouter.POST("/obtain", baseApi.ApplyWebsiteSSL) + groupRouter.POST("/download", baseApi.DownloadWebsiteSSL) + } +} diff --git a/agent/server/init.go b/agent/server/init.go new file mode 100644 index 000000000..b8ed30b47 --- /dev/null +++ b/agent/server/init.go @@ -0,0 +1,6 @@ +//go:build !xpack + +package server + +func InitOthers() { +} diff --git a/agent/server/init_xpack.go b/agent/server/init_xpack.go new file mode 120000 index 000000000..2ac522410 --- /dev/null +++ b/agent/server/init_xpack.go @@ -0,0 +1 @@ +/Users/slooop/Documents/mycode/xpack-backend/other/init_xpack.go \ No newline at end of file diff --git a/agent/server/server.go b/agent/server/server.go new file mode 100644 index 000000000..4843d5a11 --- /dev/null +++ b/agent/server/server.go @@ -0,0 +1,55 @@ +package server + +import ( + "net" + "net/http" + + "github.com/1Panel-dev/1Panel/agent/cron" + "github.com/1Panel-dev/1Panel/agent/global" + "github.com/1Panel-dev/1Panel/agent/i18n" + "github.com/1Panel-dev/1Panel/agent/init/app" + "github.com/1Panel-dev/1Panel/agent/init/business" + "github.com/1Panel-dev/1Panel/agent/init/db" + "github.com/1Panel-dev/1Panel/agent/init/hook" + "github.com/1Panel-dev/1Panel/agent/init/log" + "github.com/1Panel-dev/1Panel/agent/init/migration" + "github.com/1Panel-dev/1Panel/agent/init/router" + "github.com/1Panel-dev/1Panel/agent/init/validator" + "github.com/1Panel-dev/1Panel/agent/init/viper" + + "github.com/gin-gonic/gin" +) + +func Start() { + viper.Init() + i18n.Init() + log.Init() + db.Init() + migration.Init() + app.Init() + validator.Init() + gin.SetMode("debug") + cron.Run() + InitOthers() + business.Init() + hook.Init() + + rootRouter := router.Routers() + + server := &http.Server{ + Addr: "0.0.0.0:9998", + Handler: rootRouter, + } + ln, err := net.Listen("tcp4", "0.0.0.0:9998") + if err != nil { + panic(err) + } + type tcpKeepAliveListener struct { + *net.TCPListener + } + + global.LOG.Info("listen at http://0.0.0.0:9998") + if err := server.Serve(tcpKeepAliveListener{ln.(*net.TCPListener)}); err != nil { + panic(err) + } +} diff --git a/agent/utils/cloud_storage/client/cos.go b/agent/utils/cloud_storage/client/cos.go new file mode 100644 index 000000000..0184c8ed4 --- /dev/null +++ b/agent/utils/cloud_storage/client/cos.go @@ -0,0 +1,142 @@ +package client + +import ( + "context" + "fmt" + "net/http" + "net/url" + "os" + + cosSDK "github.com/tencentyun/cos-go-sdk-v5" +) + +type cosClient struct { + scType string + client *cosSDK.Client + clientWithBucket *cosSDK.Client +} + +func NewCosClient(vars map[string]interface{}) (*cosClient, error) { + region := loadParamFromVars("region", vars) + accessKey := loadParamFromVars("accessKey", vars) + secretKey := loadParamFromVars("secretKey", vars) + bucket := loadParamFromVars("bucket", vars) + scType := loadParamFromVars("scType", vars) + if len(scType) == 0 { + scType = "Standard" + } + + u, _ := url.Parse(fmt.Sprintf("https://cos.%s.myqcloud.com", region)) + b := &cosSDK.BaseURL{BucketURL: u} + client := cosSDK.NewClient(b, &http.Client{ + Transport: &cosSDK.AuthorizationTransport{ + SecretID: accessKey, + SecretKey: secretKey, + }, + }) + + if len(bucket) != 0 { + u2, _ := url.Parse(fmt.Sprintf("https://%s.cos.%s.myqcloud.com", bucket, region)) + b2 := &cosSDK.BaseURL{BucketURL: u2} + clientWithBucket := cosSDK.NewClient(b2, &http.Client{ + Transport: &cosSDK.AuthorizationTransport{ + SecretID: accessKey, + SecretKey: secretKey, + }, + }) + return &cosClient{client: client, clientWithBucket: clientWithBucket, scType: scType}, nil + } + + return &cosClient{client: client, clientWithBucket: nil, scType: scType}, nil +} + +func (c cosClient) ListBuckets() ([]interface{}, error) { + buckets, _, err := c.client.Service.Get(context.Background()) + if err != nil { + return nil, err + } + var datas []interface{} + for _, bucket := range buckets.Buckets { + datas = append(datas, bucket.Name) + } + return datas, nil +} + +func (c cosClient) Exist(path string) (bool, error) { + exist, err := c.clientWithBucket.Object.IsExist(context.Background(), path) + if err != nil { + return false, err + } + return exist, nil +} + +func (c cosClient) Size(path string) (int64, error) { + data, _, err := c.clientWithBucket.Bucket.Get(context.Background(), &cosSDK.BucketGetOptions{Prefix: path}) + if err != nil { + return 0, err + } + if len(data.Contents) == 0 { + return 0, fmt.Errorf("no such file %s", path) + } + return data.Contents[0].Size, nil +} + +func (c cosClient) Delete(path string) (bool, error) { + if _, err := c.clientWithBucket.Object.Delete(context.Background(), path); err != nil { + return false, err + } + return true, nil +} + +func (c cosClient) Upload(src, target string) (bool, error) { + fileInfo, err := os.Stat(src) + if err != nil { + return false, err + } + if fileInfo.Size() > 5368709120 { + opt := &cosSDK.MultiUploadOptions{ + OptIni: &cosSDK.InitiateMultipartUploadOptions{ + ACLHeaderOptions: nil, + ObjectPutHeaderOptions: &cosSDK.ObjectPutHeaderOptions{ + XCosStorageClass: c.scType, + }, + }, + PartSize: 200, + } + if _, _, err := c.clientWithBucket.Object.MultiUpload( + context.Background(), target, src, opt, + ); err != nil { + return false, err + } + return true, nil + } + if _, err := c.clientWithBucket.Object.PutFromFile(context.Background(), target, src, &cosSDK.ObjectPutOptions{ + ACLHeaderOptions: nil, + ObjectPutHeaderOptions: &cosSDK.ObjectPutHeaderOptions{ + XCosStorageClass: c.scType, + }, + }); err != nil { + return false, err + } + return true, nil +} + +func (c cosClient) Download(src, target string) (bool, error) { + if _, err := c.clientWithBucket.Object.Download(context.Background(), src, target, &cosSDK.MultiDownloadOptions{}); err != nil { + return false, err + } + return true, nil +} + +func (c cosClient) ListObjects(prefix string) ([]string, error) { + datas, _, err := c.clientWithBucket.Bucket.Get(context.Background(), &cosSDK.BucketGetOptions{Prefix: prefix}) + if err != nil { + return nil, err + } + + var result []string + for _, item := range datas.Contents { + result = append(result, item.Key) + } + return result, nil +} diff --git a/agent/utils/cloud_storage/client/helper.go b/agent/utils/cloud_storage/client/helper.go new file mode 100644 index 000000000..f354de79b --- /dev/null +++ b/agent/utils/cloud_storage/client/helper.go @@ -0,0 +1,18 @@ +package client + +import ( + "fmt" + + "github.com/1Panel-dev/1Panel/agent/global" +) + +func loadParamFromVars(key string, vars map[string]interface{}) string { + if _, ok := vars[key]; !ok { + if key != "bucket" && key != "port" { + global.LOG.Errorf("load param %s from vars failed, err: not exist!", key) + } + return "" + } + + return fmt.Sprintf("%v", vars[key]) +} diff --git a/agent/utils/cloud_storage/client/kodo.go b/agent/utils/cloud_storage/client/kodo.go new file mode 100644 index 000000000..2038cc457 --- /dev/null +++ b/agent/utils/cloud_storage/client/kodo.go @@ -0,0 +1,122 @@ +package client + +import ( + "context" + "strconv" + "time" + + "github.com/1Panel-dev/1Panel/agent/utils/files" + "github.com/qiniu/go-sdk/v7/auth" + "github.com/qiniu/go-sdk/v7/storage" +) + +type kodoClient struct { + bucket string + domain string + timeout string + auth *auth.Credentials + client *storage.BucketManager +} + +func NewKodoClient(vars map[string]interface{}) (*kodoClient, error) { + accessKey := loadParamFromVars("accessKey", vars) + secretKey := loadParamFromVars("secretKey", vars) + bucket := loadParamFromVars("bucket", vars) + domain := loadParamFromVars("domain", vars) + timeout := loadParamFromVars("timeout", vars) + if timeout == "" { + timeout = "1" + } + conn := auth.New(accessKey, secretKey) + cfg := storage.Config{ + UseHTTPS: false, + } + bucketManager := storage.NewBucketManager(conn, &cfg) + + return &kodoClient{client: bucketManager, auth: conn, bucket: bucket, domain: domain, timeout: timeout}, nil +} + +func (k kodoClient) ListBuckets() ([]interface{}, error) { + buckets, err := k.client.Buckets(true) + if err != nil { + return nil, err + } + var datas []interface{} + for _, bucket := range buckets { + datas = append(datas, bucket) + } + return datas, nil +} + +func (k kodoClient) Exist(path string) (bool, error) { + if _, err := k.client.Stat(k.bucket, path); err != nil { + return false, err + } + return true, nil +} + +func (k kodoClient) Size(path string) (int64, error) { + file, err := k.client.Stat(k.bucket, path) + if err != nil { + return 0, err + } + return file.Fsize, nil +} + +func (k kodoClient) Delete(path string) (bool, error) { + if err := k.client.Delete(k.bucket, path); err != nil { + return false, err + } + return true, nil +} + +func (k kodoClient) Upload(src, target string) (bool, error) { + + int64Value, _ := strconv.ParseInt(k.timeout, 10, 64) + unixTimestamp := int64Value * 3600 + + putPolicy := storage.PutPolicy{ + Scope: k.bucket, + Expires: uint64(unixTimestamp), + } + upToken := putPolicy.UploadToken(k.auth) + cfg := storage.Config{UseHTTPS: true, UseCdnDomains: false} + resumeUploader := storage.NewResumeUploaderV2(&cfg) + ret := storage.PutRet{} + putExtra := storage.RputV2Extra{} + if err := resumeUploader.PutFile(context.Background(), &ret, upToken, target, src, &putExtra); err != nil { + return false, err + } + return true, nil +} + +func (k kodoClient) Download(src, target string) (bool, error) { + deadline := time.Now().Add(time.Second * 3600).Unix() + privateAccessURL := storage.MakePrivateURL(k.auth, k.domain, src, deadline) + + fo := files.NewFileOp() + if err := fo.DownloadFile(privateAccessURL, target); err != nil { + return false, err + } + return true, nil +} + +func (k kodoClient) ListObjects(prefix string) ([]string, error) { + var result []string + marker := "" + for { + entries, _, nextMarker, hashNext, err := k.client.ListFiles(k.bucket, prefix, "", marker, 1000) + if err != nil { + return nil, err + } + for _, entry := range entries { + result = append(result, entry.Key) + } + if hashNext { + marker = nextMarker + } else { + break + } + } + return result, nil +} diff --git a/agent/utils/cloud_storage/client/local.go b/agent/utils/cloud_storage/client/local.go new file mode 100644 index 000000000..7703de3d2 --- /dev/null +++ b/agent/utils/cloud_storage/client/local.go @@ -0,0 +1,97 @@ +package client + +import ( + "fmt" + "os" + "path" + "path/filepath" + + "github.com/1Panel-dev/1Panel/agent/utils/common" +) + +type localClient struct { + dir string +} + +func NewLocalClient(vars map[string]interface{}) (*localClient, error) { + dir := loadParamFromVars("dir", vars) + return &localClient{dir: dir}, nil +} + +func (c localClient) ListBuckets() ([]interface{}, error) { + return nil, nil +} + +func (c localClient) Exist(file string) (bool, error) { + _, err := os.Stat(path.Join(c.dir, file)) + return err == nil, err +} + +func (c localClient) Size(file string) (int64, error) { + fileInfo, err := os.Stat(path.Join(c.dir, file)) + if err != nil { + return 0, err + } + return fileInfo.Size(), nil +} + +func (c localClient) Delete(file string) (bool, error) { + if err := os.RemoveAll(path.Join(c.dir, file)); err != nil { + return false, err + } + return true, nil +} + +func (c localClient) Upload(src, target string) (bool, error) { + targetFilePath := path.Join(c.dir, target) + if _, err := os.Stat(path.Dir(targetFilePath)); err != nil { + if os.IsNotExist(err) { + if err = os.MkdirAll(path.Dir(targetFilePath), os.ModePerm); err != nil { + return false, err + } + } else { + return false, err + } + } + + if err := common.CopyFile(src, targetFilePath); err != nil { + return false, fmt.Errorf("cp file failed, err: %v", err) + } + return true, nil +} + +func (c localClient) Download(src, target string) (bool, error) { + localPath := path.Join(c.dir, src) + if _, err := os.Stat(path.Dir(target)); err != nil { + if os.IsNotExist(err) { + if err = os.MkdirAll(path.Dir(target), os.ModePerm); err != nil { + return false, err + } + } else { + return false, err + } + } + + if err := common.CopyFile(localPath, target); err != nil { + return false, fmt.Errorf("cp file failed, err: %v", err) + } + return true, nil +} + +func (c localClient) ListObjects(prefix string) ([]string, error) { + var files []string + itemPath := path.Join(c.dir, prefix) + if _, err := os.Stat(itemPath); err != nil { + return files, nil + } + if err := filepath.Walk(itemPath, func(path string, info os.FileInfo, err error) error { + if !info.IsDir() { + files = append(files, info.Name()) + } + return nil + }); err != nil { + return nil, err + } + + return files, nil +} diff --git a/agent/utils/cloud_storage/client/minio.go b/agent/utils/cloud_storage/client/minio.go new file mode 100644 index 000000000..bf608af51 --- /dev/null +++ b/agent/utils/cloud_storage/client/minio.go @@ -0,0 +1,149 @@ +package client + +import ( + "context" + "crypto/tls" + "io" + "net/http" + "os" + "strings" + + "github.com/1Panel-dev/1Panel/agent/constant" + "github.com/minio/minio-go/v7" + "github.com/minio/minio-go/v7/pkg/credentials" +) + +type minIoClient struct { + bucket string + client *minio.Client +} + +func NewMinIoClient(vars map[string]interface{}) (*minIoClient, error) { + endpoint := loadParamFromVars("endpoint", vars) + accessKeyID := loadParamFromVars("accessKey", vars) + secretAccessKey := loadParamFromVars("secretKey", vars) + bucket := loadParamFromVars("bucket", vars) + ssl := strings.Split(endpoint, ":")[0] + if len(ssl) == 0 || (ssl != "https" && ssl != "http") { + return nil, constant.ErrInvalidParams + } + + secure := false + tlsConfig := &tls.Config{} + if ssl == "https" { + secure = true + tlsConfig.InsecureSkipVerify = true + } + var transport http.RoundTripper = &http.Transport{ + TLSClientConfig: tlsConfig, + } + client, err := minio.New(strings.ReplaceAll(endpoint, ssl+"://", ""), &minio.Options{ + Creds: credentials.NewStaticV4(accessKeyID, secretAccessKey, ""), + Secure: secure, + Transport: transport, + }) + if err != nil { + return nil, err + } + return &minIoClient{bucket: bucket, client: client}, nil +} + +func (m minIoClient) ListBuckets() ([]interface{}, error) { + buckets, err := m.client.ListBuckets(context.Background()) + if err != nil { + return nil, err + } + var result []interface{} + for _, bucket := range buckets { + result = append(result, bucket.Name) + } + return result, err +} + +func (m minIoClient) Exist(path string) (bool, error) { + if _, err := m.client.GetObject(context.Background(), m.bucket, path, minio.GetObjectOptions{}); err != nil { + return false, err + } + return true, nil +} + +func (m minIoClient) Size(path string) (int64, error) { + obj, err := m.client.GetObject(context.Background(), m.bucket, path, minio.GetObjectOptions{}) + if err != nil { + return 0, err + } + file, err := obj.Stat() + if err != nil { + return 0, err + } + return file.Size, nil +} + +func (m minIoClient) Delete(path string) (bool, error) { + object, err := m.client.GetObject(context.Background(), m.bucket, path, minio.GetObjectOptions{}) + if err != nil { + return false, err + } + info, err := object.Stat() + if err != nil { + return false, err + } + if err = m.client.RemoveObject(context.Background(), m.bucket, path, minio.RemoveObjectOptions{ + GovernanceBypass: true, + VersionID: info.VersionID, + }); err != nil { + return false, err + } + return true, nil +} + +func (m minIoClient) Upload(src, target string) (bool, error) { + file, err := os.Open(src) + if err != nil { + return false, err + } + defer file.Close() + + fileStat, err := file.Stat() + if err != nil { + return false, err + } + _, err = m.client.PutObject(context.Background(), m.bucket, target, file, fileStat.Size(), minio.PutObjectOptions{ContentType: "application/octet-stream"}) + if err != nil { + return false, err + } + return true, nil +} + +func (m minIoClient) Download(src, target string) (bool, error) { + object, err := m.client.GetObject(context.Background(), m.bucket, src, minio.GetObjectOptions{}) + if err != nil { + return false, err + } + defer object.Close() + localFile, err := os.Create(target) + if err != nil { + return false, err + } + defer localFile.Close() + if _, err = io.Copy(localFile, object); err != nil { + return false, err + } + return true, nil +} + +func (m minIoClient) ListObjects(prefix string) ([]string, error) { + opts := minio.ListObjectsOptions{ + Recursive: true, + Prefix: prefix, + } + + var result []string + for object := range m.client.ListObjects(context.Background(), m.bucket, opts) { + if object.Err != nil { + continue + } + result = append(result, object.Key) + } + return result, nil +} diff --git a/agent/utils/cloud_storage/client/onedrive.go b/agent/utils/cloud_storage/client/onedrive.go new file mode 100644 index 000000000..b786d09a5 --- /dev/null +++ b/agent/utils/cloud_storage/client/onedrive.go @@ -0,0 +1,405 @@ +package client + +import ( + "bufio" + "bytes" + "context" + "crypto/tls" + "encoding/json" + "errors" + "fmt" + "io" + "net/http" + "net/url" + "os" + "path" + "strconv" + "strings" + "time" + + "github.com/1Panel-dev/1Panel/agent/utils/files" + odsdk "github.com/goh-chunlin/go-onedrive/onedrive" + "golang.org/x/oauth2" +) + +type oneDriveClient struct { + client odsdk.Client +} + +func NewOneDriveClient(vars map[string]interface{}) (*oneDriveClient, error) { + token, err := RefreshToken("refresh_token", "accessToken", vars) + if err != nil { + return nil, err + } + isCN := loadParamFromVars("isCN", vars) + ctx := context.Background() + ts := oauth2.StaticTokenSource( + &oauth2.Token{AccessToken: token}, + ) + tc := oauth2.NewClient(ctx, ts) + + client := odsdk.NewClient(tc) + if isCN == "true" { + client.BaseURL, _ = url.Parse("https://microsoftgraph.chinacloudapi.cn/v1.0/") + } + return &oneDriveClient{client: *client}, nil +} + +func (o oneDriveClient) ListBuckets() ([]interface{}, error) { + return nil, nil +} + +func (o oneDriveClient) Exist(path string) (bool, error) { + path = "/" + strings.TrimPrefix(path, "/") + fileID, err := o.loadIDByPath(path) + if err != nil { + return false, err + } + + return len(fileID) != 0, nil +} + +func (o oneDriveClient) Size(path string) (int64, error) { + path = "/" + strings.TrimPrefix(path, "/") + pathItem := "root:" + path + if path == "/" { + pathItem = "root" + } + req, err := o.client.NewRequest("GET", fmt.Sprintf("me/drive/%s", pathItem), nil) + if err != nil { + return 0, fmt.Errorf("new request for file id failed, err: %v", err) + } + var driveItem myDriverItem + if err := o.client.Do(context.Background(), req, false, &driveItem); err != nil { + return 0, fmt.Errorf("do request for file id failed, err: %v", err) + } + + return driveItem.Size, nil +} + +type myDriverItem struct { + Name string `json:"name"` + Id string `json:"id"` + Size int64 `json:"size"` +} + +func (o oneDriveClient) Delete(path string) (bool, error) { + path = "/" + strings.TrimPrefix(path, "/") + req, err := o.client.NewRequest("DELETE", fmt.Sprintf("me/drive/root:%s", path), nil) + if err != nil { + return false, fmt.Errorf("new request for delete file failed, err: %v \n", err) + } + if err := o.client.Do(context.Background(), req, false, nil); err != nil { + return false, fmt.Errorf("do request for delete file failed, err: %v \n", err) + } + + return true, nil +} + +func (o oneDriveClient) Upload(src, target string) (bool, error) { + target = "/" + strings.TrimPrefix(target, "/") + if _, err := o.loadIDByPath(path.Dir(target)); err != nil { + if !strings.Contains(err.Error(), "itemNotFound") { + return false, err + } + if err := o.createFolder(path.Dir(target)); err != nil { + return false, fmt.Errorf("create dir before upload failed, err: %v", err) + } + } + + ctx := context.Background() + folderID, err := o.loadIDByPath(path.Dir(target)) + if err != nil { + return false, err + } + fileInfo, err := os.Stat(src) + if err != nil { + return false, err + } + if fileInfo.IsDir() { + return false, errors.New("only file is allowed to be uploaded here") + } + var isOk bool + if fileInfo.Size() < 4*1024*1024 { + isOk, err = o.upSmall(src, folderID, fileInfo.Size()) + } else { + isOk, err = o.upBig(ctx, src, folderID, fileInfo.Size()) + } + return isOk, err +} + +func (o oneDriveClient) Download(src, target string) (bool, error) { + src = "/" + strings.TrimPrefix(src, "/") + req, err := o.client.NewRequest("GET", fmt.Sprintf("me/drive/root:%s", src), nil) + if err != nil { + return false, fmt.Errorf("new request for file id failed, err: %v", err) + } + var driveItem *odsdk.DriveItem + if err := o.client.Do(context.Background(), req, false, &driveItem); err != nil { + return false, fmt.Errorf("do request for file id failed, err: %v", err) + } + + resp, err := http.Get(driveItem.DownloadURL) + if err != nil { + return false, err + } + defer resp.Body.Close() + + out, err := os.Create(target) + if err != nil { + return false, err + } + defer out.Close() + buffer := make([]byte, 2*1024*1024) + + _, err = io.CopyBuffer(out, resp.Body, buffer) + if err != nil { + return false, err + } + + return true, nil +} + +func (o *oneDriveClient) ListObjects(prefix string) ([]string, error) { + prefix = "/" + strings.TrimPrefix(prefix, "/") + folderID, err := o.loadIDByPath(prefix) + if err != nil { + return nil, err + } + + req, err := o.client.NewRequest("GET", fmt.Sprintf("me/drive/items/%s/children", folderID), nil) + if err != nil { + return nil, fmt.Errorf("new request for list failed, err: %v", err) + } + var driveItems *odsdk.OneDriveDriveItemsResponse + if err := o.client.Do(context.Background(), req, false, &driveItems); err != nil { + return nil, fmt.Errorf("do request for list failed, err: %v", err) + } + + var itemList []string + for _, item := range driveItems.DriveItems { + itemList = append(itemList, item.Name) + } + return itemList, nil +} + +func (o *oneDriveClient) loadIDByPath(path string) (string, error) { + pathItem := "root:" + path + if path == "/" { + pathItem = "root" + } + req, err := o.client.NewRequest("GET", fmt.Sprintf("me/drive/%s", pathItem), nil) + if err != nil { + return "", fmt.Errorf("new request for file id failed, err: %v", err) + } + var driveItem *odsdk.DriveItem + if err := o.client.Do(context.Background(), req, false, &driveItem); err != nil { + return "", fmt.Errorf("do request for file id failed, err: %v", err) + } + return driveItem.Id, nil +} + +func RefreshToken(grantType string, tokenType string, varMap map[string]interface{}) (string, error) { + data := url.Values{} + isCN := loadParamFromVars("isCN", varMap) + data.Set("client_id", loadParamFromVars("client_id", varMap)) + data.Set("client_secret", loadParamFromVars("client_secret", varMap)) + if grantType == "refresh_token" { + data.Set("grant_type", "refresh_token") + data.Set("refresh_token", loadParamFromVars("refresh_token", varMap)) + } else { + data.Set("grant_type", "authorization_code") + data.Set("code", loadParamFromVars("code", varMap)) + } + data.Set("redirect_uri", loadParamFromVars("redirect_uri", varMap)) + client := &http.Client{} + url := "https://login.microsoftonline.com/common/oauth2/v2.0/token" + if isCN == "true" { + url = "https://login.chinacloudapi.cn/common/oauth2/v2.0/token" + } + req, err := http.NewRequest("POST", url, strings.NewReader(data.Encode())) + if err != nil { + return "", fmt.Errorf("new http post client for access token failed, err: %v", err) + } + req.Header.Add("Content-Type", "application/x-www-form-urlencoded") + resp, err := client.Do(req) + if err != nil { + return "", fmt.Errorf("request for access token failed, err: %v", err) + } + defer resp.Body.Close() + respBody, err := io.ReadAll(resp.Body) + if err != nil { + return "", fmt.Errorf("read data from response body failed, err: %v", err) + } + + tokenMap := map[string]interface{}{} + if err := json.Unmarshal(respBody, &tokenMap); err != nil { + return "", fmt.Errorf("unmarshal data from response body failed, err: %v", err) + } + if tokenType == "accessToken" { + accessToken, ok := tokenMap["access_token"].(string) + if !ok { + return "", errors.New("no such access token in response") + } + tokenMap = nil + return accessToken, nil + } + refreshToken, ok := tokenMap["refresh_token"].(string) + if !ok { + return "", errors.New("no such access token in response") + } + tokenMap = nil + return refreshToken, nil +} + +func (o *oneDriveClient) createFolder(parent string) error { + if _, err := o.loadIDByPath(path.Dir(parent)); err != nil { + if !strings.Contains(err.Error(), "itemNotFound") { + return err + } + _ = o.createFolder(path.Dir(parent)) + } + item2, err := o.loadIDByPath(path.Dir(parent)) + if err != nil { + return err + } + if _, err := o.client.DriveItems.CreateNewFolder(context.Background(), "", item2, path.Base(parent)); err != nil { + return err + } + return nil +} + +type NewUploadSessionCreationRequest struct { + ConflictBehavior string `json:"@microsoft.graph.conflictBehavior,omitempty"` +} +type NewUploadSessionCreationResponse struct { + UploadURL string `json:"uploadUrl"` + ExpirationDateTime string `json:"expirationDateTime"` +} +type UploadSessionUploadResponse struct { + ExpirationDateTime string `json:"expirationDateTime"` + NextExpectedRanges []string `json:"nextExpectedRanges"` + DriveItem +} +type DriveItem struct { + Name string `json:"name"` + Id string `json:"id"` + DownloadURL string `json:"@microsoft.graph.downloadUrl"` + Description string `json:"description"` + Size int64 `json:"size"` + WebURL string `json:"webUrl"` +} + +func (o *oneDriveClient) NewSessionFileUploadRequest(absoluteUrl string, grandOffset, grandTotalSize int64, byteReader *bytes.Reader) (*http.Request, error) { + apiUrl, err := o.client.BaseURL.Parse(absoluteUrl) + if err != nil { + return nil, err + } + absoluteUrl = apiUrl.String() + contentLength := byteReader.Size() + req, err := http.NewRequest("PUT", absoluteUrl, byteReader) + req.Header.Set("Content-Length", strconv.FormatInt(contentLength, 10)) + preliminaryLength := grandOffset + preliminaryRange := grandOffset + contentLength - 1 + if preliminaryRange >= grandTotalSize { + preliminaryRange = grandTotalSize - 1 + preliminaryLength = preliminaryRange - grandOffset + 1 + } + req.Header.Set("Content-Range", fmt.Sprintf("bytes %d-%d/%d", preliminaryLength, preliminaryRange, grandTotalSize)) + + return req, err +} + +func (o *oneDriveClient) upSmall(srcPath, folderID string, fileSize int64) (bool, error) { + file, err := os.Open(srcPath) + if err != nil { + return false, err + } + defer file.Close() + + buffer := make([]byte, fileSize) + _, _ = file.Read(buffer) + fileReader := bytes.NewReader(buffer) + apiURL := fmt.Sprintf("me/drive/items/%s:/%s:/content?@microsoft.graph.conflictBehavior=rename", url.PathEscape(folderID), path.Base(srcPath)) + + mimeType := files.GetMimeType(srcPath) + req, err := o.client.NewFileUploadRequest(apiURL, mimeType, fileReader) + if err != nil { + return false, err + } + var response *DriveItem + if err := o.client.Do(context.Background(), req, false, &response); err != nil { + return false, fmt.Errorf("do request for list failed, err: %v", err) + } + return true, nil +} + +func (o *oneDriveClient) upBig(ctx context.Context, srcPath, folderID string, fileSize int64) (bool, error) { + file, err := os.Open(srcPath) + if err != nil { + return false, err + } + defer file.Close() + + apiURL := fmt.Sprintf("me/drive/items/%s:/%s:/createUploadSession", url.PathEscape(folderID), path.Base(srcPath)) + sessionCreationRequestInside := NewUploadSessionCreationRequest{ + ConflictBehavior: "rename", + } + + sessionCreationRequest := struct { + Item NewUploadSessionCreationRequest `json:"item"` + DeferCommit bool `json:"deferCommit"` + }{sessionCreationRequestInside, false} + + sessionCreationReq, err := o.client.NewRequest("POST", apiURL, sessionCreationRequest) + if err != nil { + return false, err + } + + var sessionCreationResp *NewUploadSessionCreationResponse + err = o.client.Do(ctx, sessionCreationReq, false, &sessionCreationResp) + if err != nil { + return false, fmt.Errorf("session creation failed %w", err) + } + + fileSessionUploadUrl := sessionCreationResp.UploadURL + + sizePerSplit := int64(5 * 1024 * 1024) + buffer := make([]byte, 5*1024*1024) + splitCount := fileSize / sizePerSplit + if fileSize%sizePerSplit != 0 { + splitCount += 1 + } + bfReader := bufio.NewReader(file) + 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 { + return false, err + } + if int64(length) < sizePerSplit { + bufferLast := buffer[:length] + buffer = bufferLast + } + sessionFileUploadReq, err := o.NewSessionFileUploadRequest(fileSessionUploadUrl, splitNow*sizePerSplit, fileSize, bytes.NewReader(buffer)) + if err != nil { + return false, err + } + res, err := httpClient.Do(sessionFileUploadReq) + if err != nil { + return false, err + } + res.Body.Close() + if res.StatusCode != 201 && res.StatusCode != 202 && res.StatusCode != 200 { + data, _ := io.ReadAll(res.Body) + return false, errors.New(string(data)) + } + } + return true, nil +} diff --git a/agent/utils/cloud_storage/client/oss.go b/agent/utils/cloud_storage/client/oss.go new file mode 100644 index 000000000..e4fed15d7 --- /dev/null +++ b/agent/utils/cloud_storage/client/oss.go @@ -0,0 +1,118 @@ +package client + +import ( + "fmt" + + osssdk "github.com/aliyun/aliyun-oss-go-sdk/oss" +) + +type ossClient struct { + scType string + bucketStr string + client osssdk.Client +} + +func NewOssClient(vars map[string]interface{}) (*ossClient, error) { + endpoint := loadParamFromVars("endpoint", vars) + accessKey := loadParamFromVars("accessKey", vars) + secretKey := loadParamFromVars("secretKey", vars) + bucketStr := loadParamFromVars("bucket", vars) + scType := loadParamFromVars("scType", vars) + if len(scType) == 0 { + scType = "Standard" + } + client, err := osssdk.New(endpoint, accessKey, secretKey) + if err != nil { + return nil, err + } + + return &ossClient{scType: scType, bucketStr: bucketStr, client: *client}, nil +} + +func (o ossClient) ListBuckets() ([]interface{}, error) { + response, err := o.client.ListBuckets() + if err != nil { + return nil, err + } + var result []interface{} + for _, bucket := range response.Buckets { + result = append(result, bucket.Name) + } + return result, err +} + +func (o ossClient) Exist(path string) (bool, error) { + bucket, err := o.client.Bucket(o.bucketStr) + if err != nil { + return false, err + } + return bucket.IsObjectExist(path) +} + +func (o ossClient) Size(path string) (int64, error) { + bucket, err := o.client.Bucket(o.bucketStr) + if err != nil { + return 0, err + } + lor, err := bucket.ListObjectsV2(osssdk.Prefix(path)) + if err != nil { + return 0, err + } + if len(lor.Objects) == 0 { + return 0, fmt.Errorf("no such file %s", path) + } + return lor.Objects[0].Size, nil +} + +func (o ossClient) Delete(path string) (bool, error) { + bucket, err := o.client.Bucket(o.bucketStr) + if err != nil { + return false, err + } + if err := bucket.DeleteObject(path); err != nil { + return false, err + } + return true, nil +} + +func (o ossClient) Upload(src, target string) (bool, error) { + bucket, err := o.client.Bucket(o.bucketStr) + if err != nil { + return false, err + } + if err := bucket.UploadFile(target, src, + 200*1024*1024, + osssdk.Routines(5), + osssdk.Checkpoint(true, ""), + osssdk.ObjectStorageClass(osssdk.StorageClassType(o.scType))); err != nil { + return false, err + } + return true, nil +} + +func (o ossClient) Download(src, target string) (bool, error) { + bucket, err := o.client.Bucket(o.bucketStr) + if err != nil { + return false, err + } + if err := bucket.DownloadFile(src, target, 200*1024*1024, osssdk.Routines(5), osssdk.Checkpoint(true, "")); err != nil { + return false, err + } + return true, nil +} + +func (o *ossClient) ListObjects(prefix string) ([]string, error) { + bucket, err := o.client.Bucket(o.bucketStr) + if err != nil { + return nil, err + } + lor, err := bucket.ListObjectsV2(osssdk.Prefix(prefix)) + if err != nil { + return nil, err + } + var result []string + for _, obj := range lor.Objects { + result = append(result, obj.Key) + } + return result, nil +} diff --git a/agent/utils/cloud_storage/client/s3.go b/agent/utils/cloud_storage/client/s3.go new file mode 100644 index 000000000..9fc7ebadd --- /dev/null +++ b/agent/utils/cloud_storage/client/s3.go @@ -0,0 +1,163 @@ +package client + +import ( + "os" + + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/aws/awserr" + "github.com/aws/aws-sdk-go/aws/credentials" + "github.com/aws/aws-sdk-go/aws/session" + "github.com/aws/aws-sdk-go/service/s3" + "github.com/aws/aws-sdk-go/service/s3/s3manager" +) + +type s3Client struct { + scType string + bucket string + Sess session.Session +} + +func NewS3Client(vars map[string]interface{}) (*s3Client, error) { + accessKey := loadParamFromVars("accessKey", vars) + secretKey := loadParamFromVars("secretKey", vars) + endpoint := loadParamFromVars("endpoint", vars) + region := loadParamFromVars("region", vars) + bucket := loadParamFromVars("bucket", vars) + scType := loadParamFromVars("scType", vars) + if len(scType) == 0 { + scType = "Standard" + } + sess, err := session.NewSession(&aws.Config{ + Credentials: credentials.NewStaticCredentials(accessKey, secretKey, ""), + Endpoint: aws.String(endpoint), + Region: aws.String(region), + DisableSSL: aws.Bool(true), + S3ForcePathStyle: aws.Bool(false), + }) + if err != nil { + return nil, err + } + return &s3Client{scType: scType, bucket: bucket, Sess: *sess}, nil +} + +func (s s3Client) ListBuckets() ([]interface{}, error) { + var result []interface{} + svc := s3.New(&s.Sess) + res, err := svc.ListBuckets(nil) + if err != nil { + return nil, err + } + for _, b := range res.Buckets { + result = append(result, b.Name) + } + return result, nil +} + +func (s s3Client) Exist(path string) (bool, error) { + svc := s3.New(&s.Sess) + if _, err := svc.HeadObject(&s3.HeadObjectInput{ + Bucket: &s.bucket, + Key: &path, + }); err != nil { + if aerr, ok := err.(awserr.RequestFailure); ok { + if aerr.StatusCode() == 404 { + return false, nil + } + } else { + return false, aerr + } + } + return true, nil +} + +func (s *s3Client) Size(path string) (int64, error) { + svc := s3.New(&s.Sess) + file, err := svc.GetObject(&s3.GetObjectInput{ + Bucket: &s.bucket, + Key: &path, + }) + if err != nil { + return 0, err + } + return *file.ContentLength, nil +} + +func (s s3Client) Delete(path string) (bool, error) { + svc := s3.New(&s.Sess) + if _, err := svc.DeleteObject(&s3.DeleteObjectInput{Bucket: aws.String(s.bucket), Key: aws.String(path)}); err != nil { + return false, err + } + if err := svc.WaitUntilObjectNotExists(&s3.HeadObjectInput{ + Bucket: aws.String(s.bucket), + Key: aws.String(path), + }); err != nil { + return false, err + } + return true, nil +} + +func (s s3Client) Upload(src, target string) (bool, error) { + fileInfo, err := os.Stat(src) + if err != nil { + return false, err + } + file, err := os.Open(src) + if err != nil { + return false, err + } + defer file.Close() + + uploader := s3manager.NewUploader(&s.Sess) + if fileInfo.Size() > s3manager.MaxUploadParts*s3manager.DefaultUploadPartSize { + uploader.PartSize = fileInfo.Size() / (s3manager.MaxUploadParts - 1) + } + if _, err := uploader.Upload(&s3manager.UploadInput{ + Bucket: aws.String(s.bucket), + Key: aws.String(target), + Body: file, + StorageClass: &s.scType, + }); err != nil { + return false, err + } + return true, nil +} + +func (s s3Client) Download(src, target string) (bool, error) { + if _, err := os.Stat(target); err != nil { + if os.IsNotExist(err) { + os.Remove(target) + } else { + return false, err + } + } + file, err := os.Create(target) + if err != nil { + return false, err + } + defer file.Close() + downloader := s3manager.NewDownloader(&s.Sess) + if _, err = downloader.Download(file, &s3.GetObjectInput{ + Bucket: aws.String(s.bucket), + Key: aws.String(src), + }); err != nil { + os.Remove(target) + return false, err + } + return true, nil +} + +func (s *s3Client) ListObjects(prefix string) ([]string, error) { + svc := s3.New(&s.Sess) + var result []string + outputs, err := svc.ListObjects(&s3.ListObjectsInput{ + Bucket: &s.bucket, + Prefix: &prefix, + }) + if err != nil { + return result, err + } + for _, item := range outputs.Contents { + result = append(result, *item.Key) + } + return result, nil +} diff --git a/agent/utils/cloud_storage/client/sftp.go b/agent/utils/cloud_storage/client/sftp.go new file mode 100644 index 000000000..3a79d9e9c --- /dev/null +++ b/agent/utils/cloud_storage/client/sftp.go @@ -0,0 +1,206 @@ +package client + +import ( + "fmt" + "io" + "net" + "os" + "path" + "time" + + "github.com/1Panel-dev/1Panel/agent/global" + "github.com/pkg/sftp" + "golang.org/x/crypto/ssh" +) + +type sftpClient struct { + bucket string + connInfo string + config *ssh.ClientConfig +} + +func NewSftpClient(vars map[string]interface{}) (*sftpClient, error) { + address := loadParamFromVars("address", vars) + port := loadParamFromVars("port", vars) + if len(port) == 0 { + global.LOG.Errorf("load param port from vars failed, err: not exist!") + } + password := loadParamFromVars("password", vars) + username := loadParamFromVars("username", vars) + bucket := loadParamFromVars("bucket", vars) + + auth := []ssh.AuthMethod{ssh.Password(password)} + clientConfig := &ssh.ClientConfig{ + User: username, + Auth: auth, + Timeout: 30 * time.Second, + HostKeyCallback: func(hostname string, remote net.Addr, key ssh.PublicKey) error { + return nil + }, + } + if _, err := ssh.Dial("tcp", fmt.Sprintf("%s:%s", address, port), clientConfig); err != nil { + return nil, err + } + + return &sftpClient{bucket: bucket, connInfo: fmt.Sprintf("%s:%s", address, port), config: clientConfig}, nil +} + +func (s sftpClient) Upload(src, target string) (bool, error) { + sshClient, err := ssh.Dial("tcp", s.connInfo, s.config) + if err != nil { + return false, err + } + defer sshClient.Close() + client, err := sftp.NewClient(sshClient) + if err != nil { + return false, err + } + defer client.Close() + + srcFile, err := os.Open(src) + if err != nil { + return false, err + } + defer srcFile.Close() + + targetFilePath := path.Join(s.bucket, target) + targetDir, _ := path.Split(targetFilePath) + if _, err = client.Stat(targetDir); err != nil { + if os.IsNotExist(err) { + if err = client.MkdirAll(targetDir); err != nil { + return false, err + } + } else { + return false, err + } + } + dstFile, err := client.Create(path.Join(s.bucket, target)) + if err != nil { + return false, err + } + defer dstFile.Close() + + if _, err := io.Copy(dstFile, srcFile); err != nil { + return false, err + } + return true, nil +} + +func (s sftpClient) ListBuckets() ([]interface{}, error) { + var result []interface{} + return result, nil +} + +func (s sftpClient) Download(src, target string) (bool, error) { + sshClient, err := ssh.Dial("tcp", s.connInfo, s.config) + if err != nil { + return false, err + } + client, err := sftp.NewClient(sshClient) + if err != nil { + return false, err + } + defer client.Close() + defer sshClient.Close() + + srcFile, err := client.Open(s.bucket + "/" + src) + if err != nil { + return false, err + } + defer srcFile.Close() + + dstFile, err := os.Create(target) + if err != nil { + return false, err + } + defer dstFile.Close() + + if _, err = io.Copy(dstFile, srcFile); err != nil { + return false, err + } + return true, err +} + +func (s sftpClient) Exist(filePath string) (bool, error) { + sshClient, err := ssh.Dial("tcp", s.connInfo, s.config) + if err != nil { + return false, err + } + client, err := sftp.NewClient(sshClient) + if err != nil { + return false, err + } + defer client.Close() + defer sshClient.Close() + + srcFile, err := client.Open(path.Join(s.bucket, filePath)) + if err != nil { + if os.IsNotExist(err) { + return false, nil + } else { + return false, err + } + } + defer srcFile.Close() + return true, err +} + +func (s sftpClient) Size(filePath string) (int64, error) { + sshClient, err := ssh.Dial("tcp", s.connInfo, s.config) + if err != nil { + return 0, err + } + client, err := sftp.NewClient(sshClient) + if err != nil { + return 0, err + } + defer client.Close() + defer sshClient.Close() + + files, err := client.Stat(path.Join(s.bucket, filePath)) + if err != nil { + return 0, err + } + return files.Size(), nil +} + +func (s sftpClient) Delete(filePath string) (bool, error) { + sshClient, err := ssh.Dial("tcp", s.connInfo, s.config) + if err != nil { + return false, err + } + client, err := sftp.NewClient(sshClient) + if err != nil { + return false, err + } + defer client.Close() + defer sshClient.Close() + + if err := client.Remove(path.Join(s.bucket, filePath)); err != nil { + return false, err + } + return true, nil +} + +func (s sftpClient) ListObjects(prefix string) ([]string, error) { + sshClient, err := ssh.Dial("tcp", s.connInfo, s.config) + if err != nil { + return nil, err + } + client, err := sftp.NewClient(sshClient) + if err != nil { + return nil, err + } + defer client.Close() + defer sshClient.Close() + + files, err := client.ReadDir(path.Join(s.bucket, prefix)) + if err != nil { + return nil, err + } + var result []string + for _, file := range files { + result = append(result, file.Name()) + } + return result, nil +} diff --git a/agent/utils/cloud_storage/client/webdav.go b/agent/utils/cloud_storage/client/webdav.go new file mode 100644 index 000000000..c1013357c --- /dev/null +++ b/agent/utils/cloud_storage/client/webdav.go @@ -0,0 +1,121 @@ +package client + +import ( + "crypto/tls" + "fmt" + "io" + "net/http" + "os" + "path" + "strings" + + "github.com/studio-b12/gowebdav" +) + +type webDAVClient struct { + Bucket string + client *gowebdav.Client +} + +func NewWebDAVClient(vars map[string]interface{}) (*webDAVClient, error) { + address := loadParamFromVars("address", vars) + port := loadParamFromVars("port", vars) + password := loadParamFromVars("password", vars) + username := loadParamFromVars("username", vars) + bucket := loadParamFromVars("bucket", vars) + + url := fmt.Sprintf("%s:%s", address, port) + if len(port) == 0 { + url = address + } + client := gowebdav.NewClient(url, username, password) + tlsConfig := &tls.Config{} + if strings.HasPrefix(address, "https") { + tlsConfig.InsecureSkipVerify = true + } + var transport http.RoundTripper = &http.Transport{ + TLSClientConfig: tlsConfig, + } + client.SetTransport(transport) + if err := client.Connect(); err != nil { + return nil, err + } + return &webDAVClient{Bucket: bucket, client: client}, nil +} + +func (s webDAVClient) Upload(src, target string) (bool, error) { + targetFilePath := path.Join(s.Bucket, target) + srcFile, err := os.Open(src) + if err != nil { + return false, err + } + defer srcFile.Close() + + if err := s.client.WriteStream(targetFilePath, srcFile, 0644); err != nil { + return false, err + } + return true, nil +} + +func (s webDAVClient) ListBuckets() ([]interface{}, error) { + var result []interface{} + return result, nil +} + +func (s webDAVClient) Download(src, target string) (bool, error) { + srcPath := path.Join(s.Bucket, src) + info, err := s.client.Stat(srcPath) + if err != nil { + return false, err + } + targetStat, err := os.Stat(target) + if err == nil { + if info.Size() == targetStat.Size() { + return true, nil + } + } + file, err := os.Create(target) + if err != nil { + return false, err + } + defer file.Close() + reader, _ := s.client.ReadStream(srcPath) + if _, err := io.Copy(file, reader); err != nil { + return false, err + } + return true, err +} + +func (s webDAVClient) Exist(pathItem string) (bool, error) { + if _, err := s.client.Stat(path.Join(s.Bucket, pathItem)); err != nil { + return false, err + } + return true, nil +} + +func (s webDAVClient) Size(pathItem string) (int64, error) { + file, err := s.client.Stat(path.Join(s.Bucket, pathItem)) + if err != nil { + return 0, err + } + return file.Size(), nil +} + +func (s webDAVClient) Delete(pathItem string) (bool, error) { + if err := s.client.Remove(path.Join(s.Bucket, pathItem)); err != nil { + return false, err + } + return true, nil +} + +func (s webDAVClient) ListObjects(prefix string) ([]string, error) { + files, err := s.client.ReadDir(path.Join(s.Bucket, prefix)) + if err != nil { + return nil, err + } + var result []string + for _, file := range files { + result = append(result, file.Name()) + } + return result, nil +} diff --git a/agent/utils/cloud_storage/cloud_storage_client.go b/agent/utils/cloud_storage/cloud_storage_client.go new file mode 100644 index 000000000..0cc233858 --- /dev/null +++ b/agent/utils/cloud_storage/cloud_storage_client.go @@ -0,0 +1,42 @@ +package cloud_storage + +import ( + "github.com/1Panel-dev/1Panel/agent/constant" + "github.com/1Panel-dev/1Panel/agent/utils/cloud_storage/client" +) + +type CloudStorageClient interface { + ListBuckets() ([]interface{}, error) + ListObjects(prefix string) ([]string, error) + Exist(path string) (bool, error) + Delete(path string) (bool, error) + Upload(src, target string) (bool, error) + Download(src, target string) (bool, error) + + Size(path string) (int64, error) +} + +func NewCloudStorageClient(backupType string, vars map[string]interface{}) (CloudStorageClient, error) { + switch backupType { + case constant.Local: + return client.NewLocalClient(vars) + case constant.S3: + return client.NewS3Client(vars) + case constant.OSS: + return client.NewOssClient(vars) + case constant.Sftp: + return client.NewSftpClient(vars) + case constant.WebDAV: + return client.NewWebDAVClient(vars) + case constant.MinIo: + return client.NewMinIoClient(vars) + case constant.Cos: + return client.NewCosClient(vars) + case constant.Kodo: + return client.NewKodoClient(vars) + case constant.OneDrive: + return client.NewOneDriveClient(vars) + default: + return nil, constant.ErrNotSupportType + } +} diff --git a/agent/utils/cmd/cmd.go b/agent/utils/cmd/cmd.go new file mode 100644 index 000000000..23799377c --- /dev/null +++ b/agent/utils/cmd/cmd.go @@ -0,0 +1,229 @@ +package cmd + +import ( + "bytes" + "context" + "errors" + "fmt" + "log" + "os" + "os/exec" + "strings" + "time" + + "github.com/1Panel-dev/1Panel/agent/buserr" + "github.com/1Panel-dev/1Panel/agent/constant" +) + +func Exec(cmdStr string) (string, error) { + return ExecWithTimeOut(cmdStr, 20*time.Second) +} + +func handleErr(stdout, stderr bytes.Buffer, err error) (string, error) { + errMsg := "" + if len(stderr.String()) != 0 { + errMsg = fmt.Sprintf("stderr: %s", stderr.String()) + } + if len(stdout.String()) != 0 { + if len(errMsg) != 0 { + errMsg = fmt.Sprintf("%s; stdout: %s", errMsg, stdout.String()) + } else { + errMsg = fmt.Sprintf("stdout: %s", stdout.String()) + } + } + return errMsg, err +} + +func ExecWithTimeOut(cmdStr string, timeout time.Duration) (string, error) { + cmd := exec.Command("bash", "-c", cmdStr) + var stdout, stderr bytes.Buffer + cmd.Stdout = &stdout + cmd.Stderr = &stderr + 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) + } + } + + return stdout.String(), nil +} + +func ExecContainerScript(containerName, cmdStr string, timeout time.Duration) error { + cmdStr = fmt.Sprintf("docker exec -i %s bash -c '%s'", containerName, cmdStr) + out, err := ExecWithTimeOut(cmdStr, timeout) + if err != nil { + if out != "" { + return fmt.Errorf("%s; err: %v", out, err) + } + return err + } + return nil +} + +func ExecCronjobWithTimeOut(cmdStr, workdir, outPath string, timeout time.Duration) error { + file, err := os.OpenFile(outPath, os.O_WRONLY|os.O_CREATE, 0666) + if err != nil { + return err + } + defer file.Close() + + cmd := exec.Command("bash", "-c", cmdStr) + cmd.Dir = workdir + cmd.Stdout = file + cmd.Stderr = file + 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 err + } + } + return nil +} + +func Execf(cmdStr string, a ...interface{}) (string, error) { + cmd := exec.Command("bash", "-c", fmt.Sprintf(cmdStr, a...)) + var stdout, stderr bytes.Buffer + cmd.Stdout = &stdout + cmd.Stderr = &stderr + err := cmd.Run() + if err != nil { + return handleErr(stdout, stderr, err) + } + return stdout.String(), nil +} + +func ExecWithCheck(name string, a ...string) (string, error) { + cmd := exec.Command(name, a...) + var stdout, stderr bytes.Buffer + cmd.Stdout = &stdout + cmd.Stderr = &stderr + err := cmd.Run() + if err != nil { + return handleErr(stdout, stderr, err) + } + return stdout.String(), nil +} + +func ExecScript(scriptPath, workDir string) (string, error) { + cmd := exec.Command("bash", scriptPath) + var stdout, stderr bytes.Buffer + cmd.Dir = workDir + cmd.Stdout = &stdout + cmd.Stderr = &stderr + 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) + } + } + + return stdout.String(), nil +} + +func ExecCmd(cmdStr string) error { + cmd := exec.Command("bash", "-c", cmdStr) + output, err := cmd.CombinedOutput() + if err != nil { + return fmt.Errorf("error : %v, output: %s", err, output) + } + return nil +} + +func ExecCmdWithDir(cmdStr, workDir string) error { + cmd := exec.Command("bash", "-c", cmdStr) + cmd.Dir = workDir + output, err := cmd.CombinedOutput() + if err != nil { + return fmt.Errorf("error : %v, output: %s", err, output) + } + return nil +} + +func CheckIllegal(args ...string) bool { + if args == nil { + return false + } + 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, "\n") || strings.Contains(arg, "\r") || strings.Contains(arg, ">") || strings.Contains(arg, "<") { + return true + } + } + return false +} + +func HasNoPasswordSudo() bool { + cmd2 := exec.Command("sudo", "-n", "ls") + err2 := cmd2.Run() + return err2 == nil +} + +func SudoHandleCmd() string { + cmd := exec.Command("sudo", "-n", "ls") + if err := cmd.Run(); err == nil { + return "sudo " + } + return "" +} + +func Which(name string) bool { + stdout, err := Execf("which %s", name) + if err != nil || (len(strings.ReplaceAll(stdout, "\n", "")) == 0 && strings.HasPrefix(stdout, "/")) { + return false + } + return true +} + +func ExecShellWithTimeOut(cmdStr, workdir string, logger *log.Logger, timeout time.Duration) error { + ctx, cancel := context.WithTimeout(context.Background(), timeout) + defer cancel() + + cmd := exec.CommandContext(ctx, "bash", "-c", cmdStr) + cmd.Dir = workdir + cmd.Stdout = logger.Writer() + cmd.Stderr = logger.Writer() + if err := cmd.Start(); err != nil { + return err + } + err := cmd.Wait() + if errors.Is(ctx.Err(), context.DeadlineExceeded) { + return buserr.New(constant.ErrCmdTimeout) + } + return err +} diff --git a/agent/utils/common/common.go b/agent/utils/common/common.go new file mode 100644 index 000000000..fb318b017 --- /dev/null +++ b/agent/utils/common/common.go @@ -0,0 +1,327 @@ +package common + +import ( + "crypto/rand" + "fmt" + "io" + mathRand "math/rand" + "net" + "os" + "path" + "reflect" + "regexp" + "sort" + "strconv" + "strings" + "time" + "unicode" + + "golang.org/x/net/idna" + + "github.com/1Panel-dev/1Panel/agent/utils/cmd" +) + +func CompareVersion(version1, version2 string) bool { + v1s := extractNumbers(version1) + v2s := extractNumbers(version2) + + maxLen := max(len(v1s), len(v2s)) + v1s = append(v1s, make([]string, maxLen-len(v1s))...) + v2s = append(v2s, make([]string, maxLen-len(v2s))...) + + for i := 0; i < maxLen; i++ { + v1, err1 := strconv.Atoi(v1s[i]) + v2, err2 := strconv.Atoi(v2s[i]) + if err1 != nil { + v1 = 0 + } + if err2 != nil { + v2 = 0 + } + if v1 != v2 { + return v1 > v2 + } + } + return false +} + +func ComparePanelVersion(version1, version2 string) bool { + if version1 == version2 { + return false + } + version1s := SplitStr(version1, ".", "-") + version2s := SplitStr(version2, ".", "-") + + if len(version2s) > len(version1s) { + for i := 0; i < len(version2s)-len(version1s); i++ { + version1s = append(version1s, "0") + } + } + if len(version1s) > len(version2s) { + for i := 0; i < len(version1s)-len(version2s); i++ { + version2s = append(version2s, "0") + } + } + + n := min(len(version1s), len(version2s)) + for i := 0; i < n; i++ { + if version1s[i] == version2s[i] { + continue + } else { + v1, err1 := strconv.Atoi(version1s[i]) + if err1 != nil { + return version1s[i] > version2s[i] + } + v2, err2 := strconv.Atoi(version2s[i]) + if err2 != nil { + return version1s[i] > version2s[i] + } + return v1 > v2 + } + } + return true +} + +func extractNumbers(version string) []string { + var numbers []string + start := -1 + for i, r := range version { + if isDigit(r) { + if start == -1 { + start = i + } + } else { + if start != -1 { + numbers = append(numbers, version[start:i]) + start = -1 + } + } + } + if start != -1 { + numbers = append(numbers, version[start:]) + } + return numbers +} + +func isDigit(r rune) bool { + return r >= '0' && r <= '9' +} + +func max(x, y int) int { + if x > y { + return x + } + return y +} + +func GetSortedVersions(versions []string) []string { + sort.Slice(versions, func(i, j int) bool { + return CompareVersion(versions[i], versions[j]) + }) + return versions +} + +func CopyFile(src, dst string) error { + source, err := os.Open(src) + if err != nil { + return err + } + defer source.Close() + + if path.Base(src) != path.Base(dst) { + dst = path.Join(dst, path.Base(src)) + } + if _, err := os.Stat(path.Dir(dst)); err != nil { + if os.IsNotExist(err) { + _ = os.MkdirAll(path.Dir(dst), os.ModePerm) + } + } + target, err := os.OpenFile(dst+"_temp", os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0755) + if err != nil { + return err + } + defer target.Close() + + if _, err = io.Copy(target, source); err != nil { + return err + } + if err = os.Rename(dst+"_temp", dst); err != nil { + return err + } + return nil +} + +func IsCrossVersion(version1, version2 string) bool { + version1s := strings.Split(version1, ".") + version2s := strings.Split(version2, ".") + v1num, _ := strconv.Atoi(version1s[0]) + v2num, _ := strconv.Atoi(version2s[0]) + return v2num > v1num +} + +func GetUuid() string { + b := make([]byte, 16) + _, _ = io.ReadFull(rand.Reader, b) + b[6] = (b[6] & 0x0f) | 0x40 + b[8] = (b[8] & 0x3f) | 0x80 + return fmt.Sprintf("%x-%x-%x-%x-%x", b[0:4], b[4:6], b[6:8], b[8:10], b[10:]) +} + +var letters = []rune("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890") + +func RandStr(n int) string { + b := make([]rune, n) + for i := range b { + b[i] = letters[mathRand.Intn(len(letters))] + } + return string(b) +} + +func RandStrAndNum(n int) string { + source := mathRand.NewSource(time.Now().UnixNano()) + randGen := mathRand.New(source) + const charset = "abcdefghijklmnopqrstuvwxyz0123456789" + b := make([]byte, n) + for i := range b { + b[i] = charset[randGen.Intn(len(charset)-1)] + } + return (string(b)) +} + +func ScanPort(port int) bool { + ln, err := net.Listen("tcp", ":"+strconv.Itoa(port)) + if err != nil { + return true + } + defer ln.Close() + return false +} + +func ScanUDPPort(port int) bool { + ln, err := net.ListenUDP("udp", &net.UDPAddr{IP: net.IPv4(127, 0, 0, 1), Port: port}) + if err != nil { + return true + } + defer ln.Close() + return false +} + +func ScanPortWithProto(port int, proto string) bool { + if proto == "udp" { + return ScanUDPPort(port) + } + return ScanPort(port) +} + +func IsNum(s string) bool { + _, err := strconv.ParseFloat(s, 64) + return err == nil +} + +func RemoveRepeatElement(a interface{}) (ret []interface{}) { + va := reflect.ValueOf(a) + for i := 0; i < va.Len(); i++ { + if i > 0 && reflect.DeepEqual(va.Index(i-1).Interface(), va.Index(i).Interface()) { + continue + } + ret = append(ret, va.Index(i).Interface()) + } + return ret +} + +func LoadSizeUnit(value float64) string { + val := int64(value) + if val%1024 != 0 { + return fmt.Sprintf("%v", val) + } + if val > 1048576 { + return fmt.Sprintf("%vM", val/1048576) + } + if val > 1024 { + return fmt.Sprintf("%vK", val/1024) + } + return fmt.Sprintf("%v", val) +} + +func LoadSizeUnit2F(value float64) string { + if value > 1073741824 { + return fmt.Sprintf("%.2fG", value/1073741824) + } + if value > 1048576 { + return fmt.Sprintf("%.2fM", value/1048576) + } + if value > 1024 { + return fmt.Sprintf("%.2fK", value/1024) + } + return fmt.Sprintf("%.2f", value) +} + +func LoadTimeZone() string { + loc := time.Now().Location() + if _, err := time.LoadLocation(loc.String()); err != nil { + return "Asia/Shanghai" + } + return loc.String() +} +func LoadTimeZoneByCmd() string { + loc := time.Now().Location().String() + if _, err := time.LoadLocation(loc); err != nil { + loc = "Asia/Shanghai" + } + std, err := cmd.Exec("timedatectl | grep 'Time zone'") + if err != nil { + return loc + } + fields := strings.Fields(string(std)) + if len(fields) != 5 { + return loc + } + if _, err := time.LoadLocation(fields[2]); err != nil { + return loc + } + return fields[2] +} + +func IsValidDomain(domain string) bool { + pattern := `^([\w\p{Han}\-\*]{1,100}\.){1,10}([\w\p{Han}\-]{1,24}|[\w\p{Han}\-]{1,24}\.[\w\p{Han}\-]{1,24})(:\d{1,5})?$` + match, err := regexp.MatchString(pattern, domain) + if err != nil { + return false + } + return match +} + +func ContainsChinese(text string) bool { + for _, char := range text { + if unicode.Is(unicode.Han, char) { + return true + } + } + return false +} + +func PunycodeEncode(text string) (string, error) { + encoder := idna.New() + ascii, err := encoder.ToASCII(text) + if err != nil { + return "", err + } + return ascii, nil +} + +func SplitStr(str string, spi ...string) []string { + lists := []string{str} + var results []string + for _, s := range spi { + results = []string{} + for _, list := range lists { + results = append(results, strings.Split(list, s)...) + } + lists = results + } + return results +} + +func IsValidIP(ip string) bool { + return net.ParseIP(ip) != nil +} diff --git a/agent/utils/compose/compose.go b/agent/utils/compose/compose.go new file mode 100644 index 000000000..3480a0d97 --- /dev/null +++ b/agent/utils/compose/compose.go @@ -0,0 +1,40 @@ +package compose + +import ( + "github.com/1Panel-dev/1Panel/agent/utils/cmd" +) + +func Pull(filePath string) (string, error) { + stdout, err := cmd.Execf("docker-compose -f %s pull", filePath) + return stdout, err +} + +func Up(filePath string) (string, error) { + stdout, err := cmd.Execf("docker-compose -f %s up -d", filePath) + return stdout, err +} + +func Down(filePath string) (string, error) { + stdout, err := cmd.Execf("docker-compose -f %s down --remove-orphans", filePath) + return stdout, err +} + +func Start(filePath string) (string, error) { + stdout, err := cmd.Execf("docker-compose -f %s start", filePath) + return stdout, err +} + +func Stop(filePath string) (string, error) { + stdout, err := cmd.Execf("docker-compose -f %s stop", filePath) + return stdout, err +} + +func Restart(filePath string) (string, error) { + stdout, err := cmd.Execf("docker-compose -f %s restart", filePath) + return stdout, err +} + +func Operate(filePath, operation string) (string, error) { + stdout, err := cmd.Execf("docker-compose -f %s %s", filePath, operation) + return stdout, err +} diff --git a/agent/utils/copier/copier.go b/agent/utils/copier/copier.go new file mode 100644 index 000000000..bfc684ce9 --- /dev/null +++ b/agent/utils/copier/copier.go @@ -0,0 +1,18 @@ +package copier + +import ( + "encoding/json" + + "github.com/pkg/errors" +) + +func Copy(to, from interface{}) error { + b, err := json.Marshal(from) + if err != nil { + return errors.Wrap(err, "marshal from data err") + } + if err = json.Unmarshal(b, to); err != nil { + return errors.Wrap(err, "unmarshal to data err") + } + return nil +} diff --git a/agent/utils/docker/compose.go b/agent/utils/docker/compose.go new file mode 100644 index 000000000..6b78c59a0 --- /dev/null +++ b/agent/utils/docker/compose.go @@ -0,0 +1,87 @@ +package docker + +import ( + "context" + "github.com/compose-spec/compose-go/v2/loader" + "github.com/compose-spec/compose-go/v2/types" + "github.com/docker/compose/v2/pkg/api" + "github.com/joho/godotenv" + "path" + "regexp" + "strings" +) + +type ComposeService struct { + api.Service + project *types.Project +} + +func GetComposeProject(projectName, workDir string, yml []byte, env []byte, skipNormalization bool) (*types.Project, error) { + var configFiles []types.ConfigFile + configFiles = append(configFiles, types.ConfigFile{ + Filename: "docker-compose.yml", + Content: yml}, + ) + envMap, err := godotenv.UnmarshalBytes(env) + if err != nil { + return nil, err + } + details := types.ConfigDetails{ + WorkingDir: workDir, + ConfigFiles: configFiles, + Environment: envMap, + } + projectName = strings.ToLower(projectName) + reg, _ := regexp.Compile(`[^a-z0-9_-]+`) + projectName = reg.ReplaceAllString(projectName, "") + project, err := loader.LoadWithContext(context.Background(), details, func(options *loader.Options) { + options.SetProjectName(projectName, true) + options.ResolvePaths = true + options.SkipNormalization = skipNormalization + }) + if err != nil { + return nil, err + } + project.ComposeFiles = []string{path.Join(workDir, "docker-compose.yml")} + return project, nil +} + +type ComposeProject struct { + Version string + Services map[string]Service `yaml:"services"` +} + +type Service struct { + Image string `yaml:"image"` +} + +func GetDockerComposeImages(projectName string, env, yml []byte) ([]string, error) { + var ( + configFiles []types.ConfigFile + images []string + ) + configFiles = append(configFiles, types.ConfigFile{ + Filename: "docker-compose.yml", + Content: yml}, + ) + envMap, err := godotenv.UnmarshalBytes(env) + if err != nil { + return nil, err + } + details := types.ConfigDetails{ + ConfigFiles: configFiles, + Environment: envMap, + } + + project, err := loader.LoadWithContext(context.Background(), details, func(options *loader.Options) { + options.SetProjectName(projectName, true) + options.ResolvePaths = true + }) + if err != nil { + return nil, err + } + for _, service := range project.AllServices() { + images = append(images, service.Image) + } + return images, nil +} diff --git a/agent/utils/docker/docker.go b/agent/utils/docker/docker.go new file mode 100644 index 000000000..94fe914cd --- /dev/null +++ b/agent/utils/docker/docker.go @@ -0,0 +1,177 @@ +package docker + +import ( + "context" + + "github.com/docker/docker/api/types/container" + "github.com/docker/docker/api/types/image" + + "github.com/1Panel-dev/1Panel/agent/app/model" + "github.com/1Panel-dev/1Panel/agent/global" + + "github.com/docker/docker/api/types" + "github.com/docker/docker/api/types/filters" + "github.com/docker/docker/client" +) + +type Client struct { + cli *client.Client +} + +func NewClient() (Client, error) { + var settingItem model.Setting + _ = global.DB.Where("key = ?", "DockerSockPath").First(&settingItem).Error + if len(settingItem.Value) == 0 { + settingItem.Value = "unix:///var/run/docker.sock" + } + cli, err := client.NewClientWithOpts(client.FromEnv, client.WithHost(settingItem.Value), client.WithAPIVersionNegotiation()) + if err != nil { + return Client{}, err + } + + return Client{ + cli: cli, + }, nil +} + +func (c Client) Close() { + _ = c.cli.Close() +} + +func NewDockerClient() (*client.Client, error) { + var settingItem model.Setting + _ = global.DB.Where("key = ?", "DockerSockPath").First(&settingItem).Error + if len(settingItem.Value) == 0 { + settingItem.Value = "unix:///var/run/docker.sock" + } + cli, err := client.NewClientWithOpts(client.FromEnv, client.WithHost(settingItem.Value), client.WithAPIVersionNegotiation()) + if err != nil { + return nil, err + } + return cli, nil +} + +func (c Client) ListContainersByName(names []string) ([]types.Container, error) { + var ( + options container.ListOptions + namesMap = make(map[string]bool) + res []types.Container + ) + options.All = true + if len(names) > 0 { + var array []filters.KeyValuePair + for _, n := range names { + namesMap["/"+n] = true + array = append(array, filters.Arg("name", n)) + } + options.Filters = filters.NewArgs(array...) + } + containers, err := c.cli.ContainerList(context.Background(), options) + if err != nil { + return nil, err + } + for _, con := range containers { + if _, ok := namesMap[con.Names[0]]; ok { + res = append(res, con) + } + } + return res, nil +} +func (c Client) ListAllContainers() ([]types.Container, error) { + var ( + options container.ListOptions + ) + options.All = true + containers, err := c.cli.ContainerList(context.Background(), options) + if err != nil { + return nil, err + } + return containers, nil +} + +func (c Client) CreateNetwork(name string) error { + _, err := c.cli.NetworkCreate(context.Background(), name, types.NetworkCreate{ + Driver: "bridge", + }) + return err +} + +func (c Client) DeleteImage(imageID string) error { + if _, err := c.cli.ImageRemove(context.Background(), imageID, image.RemoveOptions{Force: true}); err != nil { + return err + } + return nil +} + +func (c Client) InspectContainer(containerID string) (types.ContainerJSON, error) { + return c.cli.ContainerInspect(context.Background(), containerID) +} + +func (c Client) PullImage(imageName string, force bool) error { + if !force { + exist, err := c.CheckImageExist(imageName) + if err != nil { + return err + } + if exist { + return nil + } + } + if _, err := c.cli.ImagePull(context.Background(), imageName, image.PullOptions{}); err != nil { + return err + } + return nil +} + +func (c Client) GetImageIDByName(imageName string) (string, error) { + filter := filters.NewArgs() + filter.Add("reference", imageName) + list, err := c.cli.ImageList(context.Background(), image.ListOptions{ + Filters: filter, + }) + if err != nil { + return "", err + } + if len(list) > 0 { + return list[0].ID, nil + } + return "", nil +} + +func (c Client) CheckImageExist(imageName string) (bool, error) { + filter := filters.NewArgs() + filter.Add("reference", imageName) + list, err := c.cli.ImageList(context.Background(), image.ListOptions{ + Filters: filter, + }) + if err != nil { + return false, err + } + return len(list) > 0, nil +} + +func (c Client) NetworkExist(name string) bool { + var options types.NetworkListOptions + options.Filters = filters.NewArgs(filters.Arg("name", name)) + networks, err := c.cli.NetworkList(context.Background(), options) + if err != nil { + return false + } + return len(networks) > 0 +} + +func CreateDefaultDockerNetwork() error { + cli, err := NewClient() + if err != nil { + global.LOG.Errorf("init docker client error %s", err.Error()) + return err + } + defer cli.Close() + if !cli.NetworkExist("1panel-network") { + if err := cli.CreateNetwork("1panel-network"); err != nil { + global.LOG.Errorf("create default docker network error %s", err.Error()) + return err + } + } + return nil +} diff --git a/agent/utils/encrypt/encrypt.go b/agent/utils/encrypt/encrypt.go new file mode 100644 index 000000000..5c81bfa61 --- /dev/null +++ b/agent/utils/encrypt/encrypt.go @@ -0,0 +1,104 @@ +package encrypt + +import ( + "bytes" + "crypto/aes" + "crypto/cipher" + "crypto/rand" + "encoding/base64" + "fmt" + "io" + + "github.com/1Panel-dev/1Panel/agent/app/model" + "github.com/1Panel-dev/1Panel/agent/global" +) + +func StringEncrypt(text string) (string, error) { + if len(text) == 0 { + return "", nil + } + if len(global.CONF.System.EncryptKey) == 0 { + var encryptSetting model.Setting + if err := global.DB.Where("key = ?", "EncryptKey").First(&encryptSetting).Error; err != nil { + return "", err + } + global.CONF.System.EncryptKey = encryptSetting.Value + } + key := global.CONF.System.EncryptKey + pass := []byte(text) + xpass, err := aesEncryptWithSalt([]byte(key), pass) + if err == nil { + pass64 := base64.StdEncoding.EncodeToString(xpass) + return pass64, err + } + return "", err +} + +func StringDecrypt(text string) (string, error) { + if len(text) == 0 { + return "", nil + } + if len(global.CONF.System.EncryptKey) == 0 { + var encryptSetting model.Setting + if err := global.DB.Where("key = ?", "EncryptKey").First(&encryptSetting).Error; err != nil { + return "", err + } + global.CONF.System.EncryptKey = encryptSetting.Value + } + key := global.CONF.System.EncryptKey + bytesPass, err := base64.StdEncoding.DecodeString(text) + if err != nil { + return "", err + } + var tpass []byte + tpass, err = aesDecryptWithSalt([]byte(key), bytesPass) + if err == nil { + result := string(tpass[:]) + return result, err + } + return "", err +} + +func padding(plaintext []byte, blockSize int) []byte { + padding := blockSize - len(plaintext)%blockSize + padtext := bytes.Repeat([]byte{byte(padding)}, padding) + return append(plaintext, padtext...) +} + +func unPadding(origData []byte) []byte { + length := len(origData) + unpadding := int(origData[length-1]) + return origData[:(length - unpadding)] +} + +func aesEncryptWithSalt(key, plaintext []byte) ([]byte, error) { + plaintext = padding(plaintext, aes.BlockSize) + block, err := aes.NewCipher(key) + if err != nil { + return nil, err + } + ciphertext := make([]byte, aes.BlockSize+len(plaintext)) + iv := ciphertext[0:aes.BlockSize] + if _, err := io.ReadFull(rand.Reader, iv); err != nil { + return nil, err + } + cbc := cipher.NewCBCEncrypter(block, iv) + cbc.CryptBlocks(ciphertext[aes.BlockSize:], plaintext) + return ciphertext, nil +} +func aesDecryptWithSalt(key, ciphertext []byte) ([]byte, error) { + var block cipher.Block + block, err := aes.NewCipher(key) + if err != nil { + return nil, err + } + if len(ciphertext) < aes.BlockSize { + return nil, fmt.Errorf("iciphertext too short") + } + iv := ciphertext[:aes.BlockSize] + ciphertext = ciphertext[aes.BlockSize:] + cbc := cipher.NewCBCDecrypter(block, iv) + cbc.CryptBlocks(ciphertext, ciphertext) + ciphertext = unPadding(ciphertext) + return ciphertext, nil +} diff --git a/agent/utils/env/env.go b/agent/utils/env/env.go new file mode 100644 index 000000000..630511618 --- /dev/null +++ b/agent/utils/env/env.go @@ -0,0 +1,52 @@ +package env + +import ( + "fmt" + "github.com/joho/godotenv" + "os" + "sort" + "strconv" + "strings" +) + +func Write(envMap map[string]string, filename string) error { + content, err := Marshal(envMap) + if err != nil { + return err + } + file, err := os.Create(filename) + if err != nil { + return err + } + defer file.Close() + _, err = file.WriteString(content + "\n") + if err != nil { + return err + } + return file.Sync() +} + +func Marshal(envMap map[string]string) (string, error) { + lines := make([]string, 0, len(envMap)) + for k, v := range envMap { + if d, err := strconv.Atoi(v); err == nil { + lines = append(lines, fmt.Sprintf(`%s=%d`, k, d)) + } else { + lines = append(lines, fmt.Sprintf(`%s="%s"`, k, v)) + } + } + sort.Strings(lines) + return strings.Join(lines, "\n"), nil +} + +func GetEnvValueByKey(envPath, key string) (string, error) { + envMap, err := godotenv.Read(envPath) + if err != nil { + return "", err + } + value, ok := envMap[key] + if !ok { + return "", fmt.Errorf("key %s not found in %s", key, envPath) + } + return value, nil +} diff --git a/agent/utils/files/archiver.go b/agent/utils/files/archiver.go new file mode 100644 index 000000000..089709e75 --- /dev/null +++ b/agent/utils/files/archiver.go @@ -0,0 +1,38 @@ +package files + +import ( + "github.com/1Panel-dev/1Panel/agent/buserr" + "github.com/1Panel-dev/1Panel/agent/constant" + "github.com/1Panel-dev/1Panel/agent/utils/cmd" +) + +type ShellArchiver interface { + Extract(filePath, dstDir string, secret string) error + Compress(sourcePaths []string, dstFile string, secret string) error +} + +func NewShellArchiver(compressType CompressType) (ShellArchiver, error) { + switch compressType { + case Tar: + if err := checkCmdAvailability("tar"); err != nil { + return nil, err + } + return NewTarArchiver(compressType), nil + case TarGz: + return NewTarGzArchiver(), nil + case Zip: + if err := checkCmdAvailability("zip"); err != nil { + return nil, err + } + return NewZipArchiver(), nil + default: + return nil, buserr.New("unsupported compress type") + } +} + +func checkCmdAvailability(cmdStr string) error { + if cmd.Which(cmdStr) { + return nil + } + return buserr.WithName(constant.ErrCmdNotFound, cmdStr) +} diff --git a/agent/utils/files/file_op.go b/agent/utils/files/file_op.go new file mode 100644 index 000000000..8fe90bc6e --- /dev/null +++ b/agent/utils/files/file_op.go @@ -0,0 +1,666 @@ +package files + +import ( + "archive/zip" + "bufio" + "bytes" + "context" + "crypto/tls" + "encoding/json" + "fmt" + "io" + "io/fs" + "net/http" + "os" + "path" + "path/filepath" + "strconv" + "strings" + "time" + + "github.com/1Panel-dev/1Panel/agent/constant" + "github.com/1Panel-dev/1Panel/agent/utils/cmd" + http2 "github.com/1Panel-dev/1Panel/agent/utils/http" + cZip "github.com/klauspost/compress/zip" + "golang.org/x/text/encoding/simplifiedchinese" + "golang.org/x/text/transform" + + "github.com/1Panel-dev/1Panel/agent/global" + "github.com/mholt/archiver/v4" + "github.com/pkg/errors" + "github.com/spf13/afero" +) + +type FileOp struct { + Fs afero.Fs +} + +func NewFileOp() FileOp { + return FileOp{ + Fs: afero.NewOsFs(), + } +} + +func (f FileOp) OpenFile(dst string) (fs.File, error) { + return f.Fs.Open(dst) +} + +func (f FileOp) GetContent(dst string) ([]byte, error) { + afs := &afero.Afero{Fs: f.Fs} + cByte, err := afs.ReadFile(dst) + if err != nil { + return nil, err + } + return cByte, nil +} + +func (f FileOp) CreateDir(dst string, mode fs.FileMode) error { + return f.Fs.MkdirAll(dst, mode) +} + +func (f FileOp) CreateDirWithMode(dst string, mode fs.FileMode) error { + if err := f.Fs.MkdirAll(dst, mode); err != nil { + return err + } + return f.ChmodRWithMode(dst, mode, true) +} + +func (f FileOp) CreateFile(dst string) error { + if _, err := f.Fs.Create(dst); err != nil { + return err + } + return nil +} + +func (f FileOp) CreateFileWithMode(dst string, mode fs.FileMode) error { + file, err := f.Fs.OpenFile(dst, os.O_CREATE, mode) + if err != nil { + return err + } + return file.Close() +} + +func (f FileOp) LinkFile(source string, dst string, isSymlink bool) error { + if isSymlink { + osFs := afero.OsFs{} + return osFs.SymlinkIfPossible(source, dst) + } else { + return os.Link(source, dst) + } +} + +func (f FileOp) DeleteDir(dst string) error { + return f.Fs.RemoveAll(dst) +} + +func (f FileOp) Stat(dst string) bool { + info, _ := f.Fs.Stat(dst) + return info != nil +} + +func (f FileOp) DeleteFile(dst string) error { + return f.Fs.Remove(dst) +} + +func (f FileOp) CleanDir(dst string) error { + return cmd.ExecCmd(fmt.Sprintf("rm -rf %s/*", dst)) +} + +func (f FileOp) RmRf(dst string) error { + return cmd.ExecCmd(fmt.Sprintf("rm -rf %s", dst)) +} + +func (f FileOp) WriteFile(dst string, in io.Reader, mode fs.FileMode) error { + file, err := f.Fs.OpenFile(dst, os.O_RDWR|os.O_CREATE|os.O_TRUNC, mode) + if err != nil { + return err + } + defer file.Close() + + if _, err = io.Copy(file, in); err != nil { + return err + } + + if _, err = file.Stat(); err != nil { + return err + } + return nil +} + +func (f FileOp) SaveFile(dst string, content string, 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.WriteString(content) + write.Flush() + 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 { + cmdStr = fmt.Sprintf(`chown -R %s:%s "%s"`, uid, gid, dst) + } + if cmd.HasNoPasswordSudo() { + cmdStr = fmt.Sprintf("sudo %s", cmdStr) + } + if msg, err := cmd.ExecWithTimeOut(cmdStr, 10*time.Second); err != nil { + if msg != "" { + return errors.New(msg) + } + return err + } + return nil +} + +func (f FileOp) ChmodR(dst string, mode int64, sub bool) error { + cmdStr := fmt.Sprintf(`chmod %v "%s"`, fmt.Sprintf("%04o", mode), dst) + if sub { + cmdStr = fmt.Sprintf(`chmod -R %v "%s"`, fmt.Sprintf("%04o", mode), dst) + } + if cmd.HasNoPasswordSudo() { + cmdStr = fmt.Sprintf("sudo %s", cmdStr) + } + if msg, err := cmd.ExecWithTimeOut(cmdStr, 10*time.Second); err != nil { + if msg != "" { + return errors.New(msg) + } + return err + } + return nil +} + +func (f FileOp) ChmodRWithMode(dst string, mode fs.FileMode, sub bool) error { + cmdStr := fmt.Sprintf(`chmod %v "%s"`, fmt.Sprintf("%o", mode.Perm()), dst) + if sub { + cmdStr = fmt.Sprintf(`chmod -R %v "%s"`, fmt.Sprintf("%o", mode.Perm()), dst) + } + if cmd.HasNoPasswordSudo() { + cmdStr = fmt.Sprintf("sudo %s", cmdStr) + } + if msg, err := cmd.ExecWithTimeOut(cmdStr, 10*time.Second); err != nil { + if msg != "" { + return errors.New(msg) + } + return err + } + return nil +} + +func (f FileOp) Rename(oldName string, newName string) error { + return f.Fs.Rename(oldName, newName) +} + +type WriteCounter struct { + Total uint64 + Written uint64 + Key string + Name string +} + +type Process struct { + Total uint64 `json:"total"` + Written uint64 `json:"written"` + Percent float64 `json:"percent"` + Name string `json:"name"` +} + +func (w *WriteCounter) Write(p []byte) (n int, err error) { + n = len(p) + w.Written += uint64(n) + w.SaveProcess() + return n, nil +} + +func (w *WriteCounter) SaveProcess() { + percentValue := 0.0 + if w.Total > 0 { + percent := float64(w.Written) / float64(w.Total) * 100 + percentValue, _ = strconv.ParseFloat(fmt.Sprintf("%.2f", percent), 64) + } + process := Process{ + Total: w.Total, + Written: w.Written, + Percent: percentValue, + Name: w.Name, + } + by, _ := json.Marshal(process) + if percentValue < 100 { + if err := global.CACHE.Set(w.Key, string(by)); err != nil { + global.LOG.Errorf("save cache error, err %s", err.Error()) + } + } else { + if err := global.CACHE.SetWithTTL(w.Key, string(by), time.Second*time.Duration(10)); err != nil { + global.LOG.Errorf("save cache error, err %s", err.Error()) + } + } +} + +func (f FileOp) DownloadFileWithProcess(url, dst, key string, ignoreCertificate bool) error { + client := &http.Client{} + if ignoreCertificate { + client.Transport = &http.Transport{ + TLSClientConfig: &tls.Config{InsecureSkipVerify: true}, + } + } + request, err := http.NewRequest("GET", url, nil) + if err != nil { + return nil + } + request.Header.Set("Accept-Encoding", "identity") + resp, err := client.Do(request) + if err != nil { + global.LOG.Errorf("get download file [%s] error, err %s", dst, err.Error()) + return err + } + out, err := os.Create(dst) + if err != nil { + global.LOG.Errorf("create download file [%s] error, err %s", dst, err.Error()) + return err + } + go func() { + counter := &WriteCounter{} + counter.Key = key + if resp.ContentLength > 0 { + counter.Total = uint64(resp.ContentLength) + } + counter.Name = filepath.Base(dst) + if _, err = io.Copy(out, io.TeeReader(resp.Body, counter)); err != nil { + global.LOG.Errorf("save download file [%s] error, err %s", dst, err.Error()) + } + out.Close() + resp.Body.Close() + + value, err := global.CACHE.Get(counter.Key) + if err != nil { + global.LOG.Errorf("get cache error,err %s", err.Error()) + return + } + process := &Process{} + _ = json.Unmarshal(value, process) + process.Percent = 100 + process.Name = counter.Name + process.Total = process.Written + by, _ := json.Marshal(process) + if err := global.CACHE.SetWithTTL(counter.Key, string(by), time.Second*time.Duration(10)); err != nil { + global.LOG.Errorf("save cache error, err %s", err.Error()) + } + }() + return nil +} + +func (f FileOp) DownloadFile(url, dst string) error { + resp, err := http2.GetHttpRes(url) + if err != nil { + return err + } + defer resp.Body.Close() + + out, err := os.Create(dst) + if err != nil { + return fmt.Errorf("create download file [%s] error, err %s", dst, err.Error()) + } + defer out.Close() + + if _, err = io.Copy(out, resp.Body); err != nil { + return fmt.Errorf("save download file [%s] error, err %s", dst, err.Error()) + } + return nil +} + +func (f FileOp) DownloadFileWithProxy(url, dst string) error { + _, resp, err := http2.HandleGet(url, http.MethodGet, constant.TimeOut5m) + if err != nil { + return err + } + + out, err := os.Create(dst) + if err != nil { + return fmt.Errorf("create download file [%s] error, err %s", dst, err.Error()) + } + defer out.Close() + + reader := bytes.NewReader(resp) + if _, err = io.Copy(out, reader); err != nil { + return fmt.Errorf("save download file [%s] error, err %s", dst, err.Error()) + } + return nil +} + +func (f FileOp) Cut(oldPaths []string, dst, name string, cover bool) error { + for _, p := range oldPaths { + var dstPath string + if name != "" { + dstPath = filepath.Join(dst, name) + if f.Stat(dstPath) { + dstPath = dst + } + } else { + base := filepath.Base(p) + dstPath = filepath.Join(dst, base) + } + coverFlag := "" + if cover { + coverFlag = "-f" + } + + cmdStr := fmt.Sprintf(`mv %s '%s' '%s'`, coverFlag, p, dstPath) + if err := cmd.ExecCmd(cmdStr); err != nil { + return err + } + } + return nil +} + +func (f FileOp) Mv(oldPath, dstPath string) error { + cmdStr := fmt.Sprintf(`mv '%s' '%s'`, oldPath, dstPath) + if err := cmd.ExecCmd(cmdStr); err != nil { + return err + } + return nil +} + +func (f FileOp) Copy(src, dst string) error { + if src = path.Clean("/" + src); src == "" { + return os.ErrNotExist + } + if dst = path.Clean("/" + dst); dst == "" { + return os.ErrNotExist + } + if src == "/" || dst == "/" { + return os.ErrInvalid + } + if dst == src { + return os.ErrInvalid + } + info, err := f.Fs.Stat(src) + if err != nil { + return err + } + if info.IsDir() { + return f.CopyDir(src, dst) + } + return f.CopyFile(src, dst) +} + +func (f FileOp) CopyAndReName(src, dst, name string, cover bool) error { + if src = path.Clean("/" + src); src == "" { + return os.ErrNotExist + } + if dst = path.Clean("/" + dst); dst == "" { + return os.ErrNotExist + } + if src == "/" || dst == "/" { + return os.ErrInvalid + } + if dst == src { + return os.ErrInvalid + } + + srcInfo, err := f.Fs.Stat(src) + if err != nil { + return err + } + + if srcInfo.IsDir() { + dstPath := dst + if name != "" && !cover { + dstPath = filepath.Join(dst, name) + } + return cmd.ExecCmd(fmt.Sprintf(`cp -rf '%s' '%s'`, src, dstPath)) + } else { + dstPath := filepath.Join(dst, name) + if cover { + dstPath = dst + } + return cmd.ExecCmd(fmt.Sprintf(`cp -f '%s' '%s'`, src, dstPath)) + } +} + +func (f FileOp) CopyDir(src, dst string) error { + srcInfo, err := f.Fs.Stat(src) + if err != nil { + return err + } + dstDir := filepath.Join(dst, srcInfo.Name()) + if err = f.Fs.MkdirAll(dstDir, srcInfo.Mode()); err != nil { + return err + } + return cmd.ExecCmd(fmt.Sprintf(`cp -rf '%s' '%s'`, src, dst+"/")) +} + +func (f FileOp) CopyFile(src, dst string) error { + dst = filepath.Clean(dst) + string(filepath.Separator) + return cmd.ExecCmd(fmt.Sprintf(`cp -f '%s' '%s'`, src, dst+"/")) +} + +func (f FileOp) GetDirSize(path string) (float64, error) { + var size int64 + err := filepath.Walk(path, func(path string, info os.FileInfo, err error) error { + if err != nil { + return err + } + if !info.IsDir() { + size += info.Size() + } + return nil + }) + if err != nil { + return 0, err + } + return float64(size), nil +} + +func getFormat(cType CompressType) archiver.CompressedArchive { + format := archiver.CompressedArchive{} + switch cType { + case Tar: + format.Archival = archiver.Tar{} + case TarGz, Gz: + format.Compression = archiver.Gz{} + format.Archival = archiver.Tar{} + case SdkTarGz: + format.Compression = archiver.Gz{} + format.Archival = archiver.Tar{} + case SdkZip, Zip: + format.Archival = archiver.Zip{ + Compression: zip.Deflate, + } + case Bz2: + format.Compression = archiver.Bz2{} + format.Archival = archiver.Tar{} + case Xz: + format.Compression = archiver.Xz{} + format.Archival = archiver.Tar{} + } + return format +} + +func (f FileOp) Compress(srcRiles []string, dst string, name string, cType CompressType, secret string) error { + format := getFormat(cType) + + fileMaps := make(map[string]string, len(srcRiles)) + for _, s := range srcRiles { + base := filepath.Base(s) + fileMaps[s] = base + } + + if !f.Stat(dst) { + _ = f.CreateDir(dst, 0755) + } + + files, err := archiver.FilesFromDisk(nil, fileMaps) + if err != nil { + return err + } + dstFile := filepath.Join(dst, name) + out, err := f.Fs.Create(dstFile) + if err != nil { + return err + } + + switch cType { + case Zip: + if err := ZipFile(files, out); err == nil { + return nil + } + _ = f.DeleteFile(dstFile) + return NewZipArchiver().Compress(srcRiles, dstFile, "") + case TarGz: + err = NewTarGzArchiver().Compress(srcRiles, dstFile, secret) + if err != nil { + _ = f.DeleteFile(dstFile) + return err + } + default: + err = format.Archive(context.Background(), out, files) + if err != nil { + _ = f.DeleteFile(dstFile) + return err + } + } + return nil +} + +func isIgnoreFile(name string) bool { + return strings.HasPrefix(name, "__MACOSX") || strings.HasSuffix(name, ".DS_Store") || strings.HasPrefix(name, "._") +} + +func decodeGBK(input string) (string, error) { + decoder := simplifiedchinese.GBK.NewDecoder() + decoded, _, err := transform.String(decoder, input) + if err != nil { + return "", err + } + return decoded, nil +} + +func (f FileOp) decompressWithSDK(srcFile string, dst string, cType CompressType) error { + format := getFormat(cType) + handler := func(ctx context.Context, archFile archiver.File) error { + info := archFile.FileInfo + if isIgnoreFile(archFile.Name()) { + return nil + } + fileName := archFile.NameInArchive + var err error + if header, ok := archFile.Header.(cZip.FileHeader); ok { + if header.NonUTF8 && header.Flags == 0 { + fileName, err = decodeGBK(fileName) + if err != nil { + return err + } + } + } + filePath := filepath.Join(dst, fileName) + if archFile.FileInfo.IsDir() { + if err := f.Fs.MkdirAll(filePath, info.Mode()); err != nil { + return err + } + return nil + } else { + parentDir := path.Dir(filePath) + if !f.Stat(parentDir) { + if err := f.Fs.MkdirAll(parentDir, info.Mode()); err != nil { + return err + } + } + } + fr, err := archFile.Open() + if err != nil { + return err + } + defer fr.Close() + fw, err := f.Fs.OpenFile(filePath, os.O_CREATE|os.O_RDWR|os.O_TRUNC, info.Mode()) + if err != nil { + return err + } + defer fw.Close() + if _, err := io.Copy(fw, fr); err != nil { + return err + } + + return nil + } + input, err := f.Fs.Open(srcFile) + if err != nil { + return err + } + return format.Extract(context.Background(), input, nil, handler) +} + +func (f FileOp) Decompress(srcFile string, dst string, cType CompressType, secret string) error { + if cType == Tar || cType == Zip || cType == TarGz { + shellArchiver, err := NewShellArchiver(cType) + if err == nil { + if err = shellArchiver.Extract(srcFile, dst, secret); err == nil { + return nil + } + } + } + return f.decompressWithSDK(srcFile, dst, cType) +} + +func ZipFile(files []archiver.File, dst afero.File) error { + zw := zip.NewWriter(dst) + defer zw.Close() + + for _, file := range files { + hdr, err := zip.FileInfoHeader(file) + if err != nil { + return err + } + hdr.Method = zip.Deflate + hdr.Name = file.NameInArchive + if file.IsDir() { + if !strings.HasSuffix(hdr.Name, "/") { + hdr.Name += "/" + } + } + w, err := zw.CreateHeader(hdr) + if err != nil { + return err + } + if file.IsDir() { + continue + } + + if file.LinkTarget != "" { + _, err = w.Write([]byte(filepath.ToSlash(file.LinkTarget))) + if err != nil { + return err + } + } else { + fileReader, err := file.Open() + if err != nil { + return err + } + _, err = io.Copy(w, fileReader) + if err != nil { + return err + } + } + } + return nil +} diff --git a/agent/utils/files/fileinfo.go b/agent/utils/files/fileinfo.go new file mode 100644 index 000000000..c2e318976 --- /dev/null +++ b/agent/utils/files/fileinfo.go @@ -0,0 +1,426 @@ +package files + +import ( + "bufio" + "fmt" + "io/fs" + "net/http" + "os" + "os/exec" + "path" + "path/filepath" + "sort" + "strconv" + "strings" + "syscall" + "time" + + "github.com/1Panel-dev/1Panel/agent/app/repo" + "github.com/1Panel-dev/1Panel/agent/buserr" + "github.com/1Panel-dev/1Panel/agent/constant" + + "github.com/spf13/afero" +) + +type FileInfo struct { + Fs afero.Fs `json:"-"` + Path string `json:"path"` + Name string `json:"name"` + User string `json:"user"` + Group string `json:"group"` + Uid string `json:"uid"` + Gid string `json:"gid"` + Extension string `json:"extension"` + Content string `json:"content"` + Size int64 `json:"size"` + IsDir bool `json:"isDir"` + IsSymlink bool `json:"isSymlink"` + IsHidden bool `json:"isHidden"` + LinkPath string `json:"linkPath"` + Type string `json:"type"` + Mode string `json:"mode"` + MimeType string `json:"mimeType"` + UpdateTime time.Time `json:"updateTime"` + ModTime time.Time `json:"modTime"` + FileMode os.FileMode `json:"-"` + Items []*FileInfo `json:"items"` + ItemTotal int `json:"itemTotal"` + FavoriteID uint `json:"favoriteID"` + IsDetail bool `json:"isDetail"` +} + +type FileOption struct { + Path string `json:"path"` + Search string `json:"search"` + ContainSub bool `json:"containSub"` + Expand bool `json:"expand"` + Dir bool `json:"dir"` + ShowHidden bool `json:"showHidden"` + Page int `json:"page"` + PageSize int `json:"pageSize"` + SortBy string `json:"sortBy"` + SortOrder string `json:"sortOrder"` + IsDetail bool `json:"isDetail"` +} + +type FileSearchInfo struct { + Path string `json:"path"` + fs.FileInfo +} + +func NewFileInfo(op FileOption) (*FileInfo, error) { + var appFs = afero.NewOsFs() + + info, err := appFs.Stat(op.Path) + if err != nil { + return nil, err + } + + file := &FileInfo{ + Fs: appFs, + Path: op.Path, + Name: info.Name(), + IsDir: info.IsDir(), + FileMode: info.Mode(), + ModTime: info.ModTime(), + Size: info.Size(), + IsSymlink: IsSymlink(info.Mode()), + Extension: filepath.Ext(info.Name()), + IsHidden: IsHidden(op.Path), + Mode: fmt.Sprintf("%04o", info.Mode().Perm()), + User: GetUsername(info.Sys().(*syscall.Stat_t).Uid), + Uid: strconv.FormatUint(uint64(info.Sys().(*syscall.Stat_t).Uid), 10), + Gid: strconv.FormatUint(uint64(info.Sys().(*syscall.Stat_t).Gid), 10), + Group: GetGroup(info.Sys().(*syscall.Stat_t).Gid), + MimeType: GetMimeType(op.Path), + IsDetail: op.IsDetail, + } + favoriteRepo := repo.NewIFavoriteRepo() + favorite, _ := favoriteRepo.GetFirst(favoriteRepo.WithByPath(op.Path)) + if favorite.ID > 0 { + file.FavoriteID = favorite.ID + } + + if file.IsSymlink { + file.LinkPath = GetSymlink(op.Path) + } + if op.Expand { + if err := handleExpansion(file, op); err != nil { + return nil, err + } + } + return file, nil +} + +func handleExpansion(file *FileInfo, op FileOption) error { + if file.IsDir { + return file.listChildren(op) + } + + if !file.IsDetail { + return file.getContent() + } + + return nil +} + +func (f *FileInfo) search(search string, count int) (files []FileSearchInfo, total int, err error) { + cmd := exec.Command("find", f.Path, "-name", fmt.Sprintf("*%s*", search)) + output, err := cmd.StdoutPipe() + if err != nil { + return + } + if err = cmd.Start(); err != nil { + return + } + defer func() { + _ = cmd.Wait() + _ = cmd.Process.Kill() + }() + + scanner := bufio.NewScanner(output) + for scanner.Scan() { + line := scanner.Text() + info, err := os.Stat(line) + if err != nil { + continue + } + total++ + if total > count { + continue + } + files = append(files, FileSearchInfo{ + Path: line, + FileInfo: info, + }) + } + if err = scanner.Err(); err != nil { + return + } + return +} + +func sortFileList(list []FileSearchInfo, sortBy, sortOrder string) { + switch sortBy { + case "name": + if sortOrder == "ascending" { + sort.Slice(list, func(i, j int) bool { + return list[i].Name() < list[j].Name() + }) + } else { + sort.Slice(list, func(i, j int) bool { + return list[i].Name() > list[j].Name() + }) + } + case "size": + if sortOrder == "ascending" { + sort.Slice(list, func(i, j int) bool { + return list[i].Size() < list[j].Size() + }) + } else { + sort.Slice(list, func(i, j int) bool { + return list[i].Size() > list[j].Size() + }) + } + case "modTime": + if sortOrder == "ascending" { + sort.Slice(list, func(i, j int) bool { + return list[i].ModTime().Before(list[j].ModTime()) + }) + } else { + sort.Slice(list, func(i, j int) bool { + return list[i].ModTime().After(list[j].ModTime()) + }) + } + } +} + +func (f *FileInfo) listChildren(option FileOption) error { + afs := &afero.Afero{Fs: f.Fs} + var ( + files []FileSearchInfo + err error + total int + ) + + if option.Search != "" && option.ContainSub { + files, total, err = f.search(option.Search, option.Page*option.PageSize) + if err != nil { + return err + } + } else { + files, err = f.getFiles(afs, option) + if err != nil { + return err + } + } + + items, err := f.processFiles(files, option) + if err != nil { + return err + } + + if option.ContainSub { + f.ItemTotal = total + } + start := (option.Page - 1) * option.PageSize + end := option.PageSize + start + var result []*FileInfo + if start < 0 || start > f.ItemTotal || end < 0 || start > end { + result = items + } else { + if end > f.ItemTotal { + result = items[start:] + } else { + result = items[start:end] + } + } + + f.Items = result + return nil +} + +func (f *FileInfo) getFiles(afs *afero.Afero, option FileOption) ([]FileSearchInfo, error) { + dirFiles, err := afs.ReadDir(f.Path) + if err != nil { + return nil, err + } + + var ( + dirs []FileSearchInfo + fileList []FileSearchInfo + ) + + for _, file := range dirFiles { + info := FileSearchInfo{ + Path: f.Path, + FileInfo: file, + } + if file.IsDir() { + dirs = append(dirs, info) + } else { + fileList = append(fileList, info) + } + } + + sortFileList(dirs, option.SortBy, option.SortOrder) + sortFileList(fileList, option.SortBy, option.SortOrder) + + return append(dirs, fileList...), nil +} + +func (f *FileInfo) processFiles(files []FileSearchInfo, option FileOption) ([]*FileInfo, error) { + var items []*FileInfo + + for _, df := range files { + if shouldSkipFile(df, option) { + continue + } + + name, fPath := f.getFilePathAndName(option, df) + + if !option.ShowHidden && IsHidden(name) { + continue + } + f.ItemTotal++ + + isSymlink, isInvalidLink := f.checkSymlink(df) + + file := &FileInfo{ + Fs: f.Fs, + Name: name, + Size: df.Size(), + ModTime: df.ModTime(), + FileMode: df.Mode(), + IsDir: df.IsDir(), + IsSymlink: isSymlink, + IsHidden: IsHidden(fPath), + Extension: filepath.Ext(name), + Path: fPath, + Mode: fmt.Sprintf("%04o", df.Mode().Perm()), + User: GetUsername(df.Sys().(*syscall.Stat_t).Uid), + Group: GetGroup(df.Sys().(*syscall.Stat_t).Gid), + Uid: strconv.FormatUint(uint64(df.Sys().(*syscall.Stat_t).Uid), 10), + Gid: strconv.FormatUint(uint64(df.Sys().(*syscall.Stat_t).Gid), 10), + } + favoriteRepo := repo.NewIFavoriteRepo() + favorite, _ := favoriteRepo.GetFirst(favoriteRepo.WithByPath(fPath)) + if favorite.ID > 0 { + file.FavoriteID = favorite.ID + } + if isSymlink { + file.LinkPath = GetSymlink(fPath) + } + if df.Size() > 0 { + file.MimeType = GetMimeType(fPath) + } + if isInvalidLink { + file.Type = "invalid_link" + } + items = append(items, file) + } + + return items, nil +} + +func shouldSkipFile(df FileSearchInfo, option FileOption) bool { + if option.Dir && !df.IsDir() { + return true + } + + if option.Search != "" && !option.ContainSub { + lowerName := strings.ToLower(df.Name()) + lowerSearch := strings.ToLower(option.Search) + if !strings.Contains(lowerName, lowerSearch) { + return true + } + } + + return false +} + +func (f *FileInfo) getFilePathAndName(option FileOption, df FileSearchInfo) (string, string) { + name := df.Name() + fPath := path.Join(df.Path, df.Name()) + + if option.Search != "" && option.ContainSub { + fPath = df.Path + name = strings.TrimPrefix(strings.TrimPrefix(fPath, f.Path), "/") + } + + return name, fPath +} + +func (f *FileInfo) checkSymlink(df FileSearchInfo) (bool, bool) { + isSymlink := false + isInvalidLink := false + + if IsSymlink(df.Mode()) { + isSymlink = true + info, err := f.Fs.Stat(path.Join(df.Path, df.Name())) + if err == nil { + df.FileInfo = info + } else { + isInvalidLink = true + } + } + + return isSymlink, isInvalidLink +} + +func (f *FileInfo) getContent() error { + if IsBlockDevice(f.FileMode) { + return buserr.New(constant.ErrFileCanNotRead) + } + if f.Size > 10*1024*1024 { + return buserr.New("ErrFileToLarge") + } + afs := &afero.Afero{Fs: f.Fs} + cByte, err := afs.ReadFile(f.Path) + if err != nil { + return nil + } + if len(cByte) > 0 && DetectBinary(cByte) { + return buserr.New(constant.ErrFileCanNotRead) + } + f.Content = string(cByte) + return nil +} + +func DetectBinary(buf []byte) bool { + mimeType := http.DetectContentType(buf) + if !strings.HasPrefix(mimeType, "text/") { + whiteByte := 0 + n := min(1024, len(buf)) + for i := 0; i < n; i++ { + if (buf[i] >= 0x20) || buf[i] == 9 || buf[i] == 10 || buf[i] == 13 { + whiteByte++ + } else if buf[i] <= 6 || (buf[i] >= 14 && buf[i] <= 31) { + return true + } + } + return whiteByte < 1 + } + return false + +} + +func min(x, y int) int { + if x < y { + return x + } + return y +} + +type CompressType string + +const ( + Zip CompressType = "zip" + Gz CompressType = "gz" + Bz2 CompressType = "bz2" + Tar CompressType = "tar" + TarGz CompressType = "tar.gz" + Xz CompressType = "xz" + SdkZip CompressType = "sdkZip" + SdkTarGz CompressType = "sdkTarGz" +) diff --git a/agent/utils/files/tar.go b/agent/utils/files/tar.go new file mode 100644 index 000000000..521c989b7 --- /dev/null +++ b/agent/utils/files/tar.go @@ -0,0 +1,39 @@ +package files + +import ( + "fmt" + + "github.com/1Panel-dev/1Panel/agent/utils/cmd" +) + +type TarArchiver struct { + Cmd string + CompressType CompressType +} + +func NewTarArchiver(compressType CompressType) ShellArchiver { + return &TarArchiver{ + Cmd: "tar", + CompressType: compressType, + } +} + +func (t TarArchiver) Extract(FilePath string, dstDir string, secret string) error { + return cmd.ExecCmd(fmt.Sprintf("%s %s %s -C %s", t.Cmd, t.getOptionStr("extract"), FilePath, dstDir)) +} + +func (t TarArchiver) Compress(sourcePaths []string, dstFile string, secret string) error { + return nil +} + +func (t TarArchiver) getOptionStr(Option string) string { + switch t.CompressType { + case Tar: + if Option == "compress" { + return "cvf" + } else { + return "xf" + } + } + return "" +} diff --git a/agent/utils/files/tar_gz.go b/agent/utils/files/tar_gz.go new file mode 100644 index 000000000..93a82c54b --- /dev/null +++ b/agent/utils/files/tar_gz.go @@ -0,0 +1,61 @@ +package files + +import ( + "fmt" + "path/filepath" + "strings" + + "github.com/1Panel-dev/1Panel/agent/global" + "github.com/1Panel-dev/1Panel/agent/utils/cmd" +) + +type TarGzArchiver struct { +} + +func NewTarGzArchiver() ShellArchiver { + return &TarGzArchiver{} +} + +func (t TarGzArchiver) Extract(filePath, dstDir string, secret string) error { + var err error + commands := "" + if len(secret) != 0 { + extraCmd := "openssl enc -d -aes-256-cbc -k '" + secret + "' -in " + filePath + " | " + commands = fmt.Sprintf("%s tar -zxvf - -C %s", extraCmd, dstDir+" > /dev/null 2>&1") + global.LOG.Debug(strings.ReplaceAll(commands, fmt.Sprintf(" %s ", secret), "******")) + } else { + commands = fmt.Sprintf("tar -zxvf %s %s", filePath+" -C ", dstDir+" > /dev/null 2>&1") + global.LOG.Debug(commands) + } + if err = cmd.ExecCmd(commands); err != nil { + return err + } + return nil +} + +func (t TarGzArchiver) Compress(sourcePaths []string, dstFile string, secret string) error { + var err error + path := "" + itemDir := "" + for _, item := range sourcePaths { + itemDir += filepath.Base(item) + " " + } + aheadDir := dstFile[:strings.LastIndex(dstFile, "/")] + if len(aheadDir) == 0 { + aheadDir = "/" + } + path += fmt.Sprintf("- -C %s %s", aheadDir, itemDir) + commands := "" + if len(secret) != 0 { + extraCmd := "| openssl enc -aes-256-cbc -salt -k '" + secret + "' -out" + commands = fmt.Sprintf("tar -zcf %s %s %s", path, extraCmd, dstFile) + global.LOG.Debug(strings.ReplaceAll(commands, fmt.Sprintf(" %s ", secret), "******")) + } else { + commands = fmt.Sprintf("tar -zcf %s -C %s %s", dstFile, aheadDir, itemDir) + global.LOG.Debug(commands) + } + if err = cmd.ExecCmd(commands); err != nil { + return err + } + return nil +} diff --git a/agent/utils/files/utils.go b/agent/utils/files/utils.go new file mode 100644 index 000000000..76ba78156 --- /dev/null +++ b/agent/utils/files/utils.go @@ -0,0 +1,164 @@ +package files + +import ( + "bufio" + "fmt" + "io" + "net/http" + "os" + "os/user" + "path/filepath" + "strconv" + "strings" +) + +func IsSymlink(mode os.FileMode) bool { + return mode&os.ModeSymlink != 0 +} + +func IsBlockDevice(mode os.FileMode) bool { + return mode&os.ModeDevice != 0 && mode&os.ModeCharDevice == 0 +} + +func GetMimeType(path string) string { + file, err := os.Open(path) + if err != nil { + return "" + } + defer file.Close() + + buffer := make([]byte, 512) + _, err = file.Read(buffer) + if err != nil { + return "" + } + mimeType := http.DetectContentType(buffer) + return mimeType +} + +func GetSymlink(path string) string { + linkPath, err := os.Readlink(path) + if err != nil { + return "" + } + return linkPath +} + +func GetUsername(uid uint32) string { + usr, err := user.LookupId(strconv.Itoa(int(uid))) + if err != nil { + return "" + } + return usr.Username +} + +func GetGroup(gid uint32) string { + usr, err := user.LookupGroupId(strconv.Itoa(int(gid))) + if err != nil { + return "" + } + return usr.Name +} + +const dotCharacter = 46 + +func IsHidden(path string) bool { + return path[0] == dotCharacter +} + +func countLines(path string) (int, error) { + file, err := os.Open(path) + if err != nil { + return 0, err + } + defer file.Close() + + scanner := bufio.NewScanner(file) + lineCount := 0 + for scanner.Scan() { + lineCount++ + } + if err := scanner.Err(); err != nil { + return 0, err + } + return lineCount, nil +} + +func ReadFileByLine(filename string, page, pageSize int, latest bool) (lines []string, isEndOfFile bool, total int, err error) { + if !NewFileOp().Stat(filename) { + return + } + file, err := os.Open(filename) + if err != nil { + return + } + defer file.Close() + + totalLines, err := countLines(filename) + if err != nil { + return + } + total = (totalLines + pageSize - 1) / pageSize + reader := bufio.NewReaderSize(file, 8192) + + if latest { + page = total + } + currentLine := 0 + startLine := (page - 1) * pageSize + endLine := startLine + pageSize + + for { + line, _, err := reader.ReadLine() + if err == io.EOF { + break + } + if currentLine >= startLine && currentLine < endLine { + lines = append(lines, string(line)) + } + currentLine++ + if currentLine >= endLine { + break + } + } + + isEndOfFile = currentLine < endLine + return +} + +func GetParentMode(path string) (os.FileMode, error) { + absPath, err := filepath.Abs(path) + if err != nil { + return 0, err + } + + for { + fileInfo, err := os.Stat(absPath) + if err == nil { + return fileInfo.Mode() & os.ModePerm, nil + } + if !os.IsNotExist(err) { + return 0, err + } + + parentDir := filepath.Dir(absPath) + if parentDir == absPath { + return 0, fmt.Errorf("no existing directory found in the path: %s", path) + } + absPath = parentDir + } +} + +func IsInvalidChar(name string) bool { + return strings.Contains(name, "&") +} + +func IsEmptyDir(dir string) bool { + f, err := os.Open(dir) + if err != nil { + return false + } + defer f.Close() + _, err = f.Readdirnames(1) + return err == io.EOF +} diff --git a/agent/utils/files/zip.go b/agent/utils/files/zip.go new file mode 100644 index 000000000..2f3298bb2 --- /dev/null +++ b/agent/utils/files/zip.go @@ -0,0 +1,52 @@ +package files + +import ( + "fmt" + "path" + "strings" + "time" + + "github.com/1Panel-dev/1Panel/agent/constant" + "github.com/1Panel-dev/1Panel/agent/global" + "github.com/1Panel-dev/1Panel/agent/utils/cmd" + "github.com/1Panel-dev/1Panel/agent/utils/common" +) + +type ZipArchiver struct { +} + +func NewZipArchiver() ShellArchiver { + return &ZipArchiver{} +} + +func (z ZipArchiver) Extract(filePath, dstDir string, secret string) error { + if err := checkCmdAvailability("unzip"); err != nil { + return err + } + return cmd.ExecCmd(fmt.Sprintf("unzip -qo %s -d %s", filePath, dstDir)) +} + +func (z ZipArchiver) Compress(sourcePaths []string, dstFile string, _ string) error { + var err error + tmpFile := path.Join(global.CONF.System.TmpDir, fmt.Sprintf("%s%s.zip", common.RandStr(50), time.Now().Format(constant.DateTimeSlimLayout))) + op := NewFileOp() + defer func() { + _ = op.DeleteFile(tmpFile) + if err != nil { + _ = op.DeleteFile(dstFile) + } + }() + baseDir := path.Dir(sourcePaths[0]) + relativePaths := make([]string, len(sourcePaths)) + for i, sp := range sourcePaths { + relativePaths[i] = path.Base(sp) + } + cmdStr := fmt.Sprintf("zip -qr %s %s", tmpFile, strings.Join(relativePaths, " ")) + if err = cmd.ExecCmdWithDir(cmdStr, baseDir); err != nil { + return err + } + if err = op.Mv(tmpFile, dstFile); err != nil { + return err + } + return nil +} diff --git a/agent/utils/firewall/client.go b/agent/utils/firewall/client.go new file mode 100644 index 000000000..82223ced9 --- /dev/null +++ b/agent/utils/firewall/client.go @@ -0,0 +1,39 @@ +package firewall + +import ( + "os" + + "github.com/1Panel-dev/1Panel/agent/buserr" + "github.com/1Panel-dev/1Panel/agent/constant" + "github.com/1Panel-dev/1Panel/agent/utils/firewall/client" +) + +type FirewallClient interface { + Name() string // ufw firewalld + Start() error + Stop() error + Restart() error + Reload() error + Status() (string, error) // running not running + Version() (string, error) + + ListPort() ([]client.FireInfo, error) + ListForward() ([]client.FireInfo, error) + ListAddress() ([]client.FireInfo, error) + + Port(port client.FireInfo, operation string) error + RichRules(rule client.FireInfo, operation string) error + PortForward(info client.Forward, operation string) error + + EnableForward() error +} + +func NewFirewallClient() (FirewallClient, error) { + if _, err := os.Stat("/usr/sbin/firewalld"); err == nil { + return client.NewFirewalld() + } + if _, err := os.Stat("/usr/sbin/ufw"); err == nil { + return client.NewUfw() + } + return nil, buserr.New(constant.ErrFirewall) +} diff --git a/agent/utils/firewall/client/firewalld.go b/agent/utils/firewall/client/firewalld.go new file mode 100644 index 000000000..9da5cc746 --- /dev/null +++ b/agent/utils/firewall/client/firewalld.go @@ -0,0 +1,263 @@ +package client + +import ( + "fmt" + "regexp" + "strings" + "sync" + + "github.com/1Panel-dev/1Panel/agent/buserr" + "github.com/1Panel-dev/1Panel/agent/constant" + "github.com/1Panel-dev/1Panel/agent/utils/cmd" +) + +var ForwardListRegex = regexp.MustCompile(`^port=(\d{1,5}):proto=(.+?):toport=(\d{1,5}):toaddr=(.*)$`) + +type Firewall struct{} + +func NewFirewalld() (*Firewall, error) { + return &Firewall{}, nil +} + +func (f *Firewall) Name() string { + return "firewalld" +} + +func (f *Firewall) Status() (string, error) { + stdout, _ := cmd.Exec("firewall-cmd --state") + if stdout == "running\n" { + return "running", nil + } + return "not running", nil +} + +func (f *Firewall) Version() (string, error) { + stdout, err := cmd.Exec("firewall-cmd --version") + if err != nil { + return "", fmt.Errorf("load the firewall version failed, err: %s", stdout) + } + return strings.ReplaceAll(stdout, "\n ", ""), nil +} + +func (f *Firewall) Start() error { + stdout, err := cmd.Exec("systemctl start firewalld") + if err != nil { + return fmt.Errorf("enable the firewall failed, err: %s", stdout) + } + return nil +} + +func (f *Firewall) Stop() error { + stdout, err := cmd.Exec("systemctl stop firewalld") + if err != nil { + return fmt.Errorf("stop the firewall failed, err: %s", stdout) + } + return nil +} + +func (f *Firewall) Restart() error { + stdout, err := cmd.Exec("systemctl restart firewalld") + if err != nil { + return fmt.Errorf("restart the firewall failed, err: %s", stdout) + } + return nil +} + +func (f *Firewall) Reload() error { + stdout, err := cmd.Exec("firewall-cmd --reload") + if err != nil { + return fmt.Errorf("reload firewall failed, err: %s", stdout) + } + return nil +} + +func (f *Firewall) ListPort() ([]FireInfo, error) { + var wg sync.WaitGroup + var datas []FireInfo + wg.Add(2) + go func() { + defer wg.Done() + stdout, err := cmd.Exec("firewall-cmd --zone=public --list-ports") + if err != nil { + return + } + ports := strings.Split(strings.ReplaceAll(stdout, "\n", ""), " ") + for _, port := range ports { + if len(port) == 0 { + continue + } + var itemPort FireInfo + if strings.Contains(port, "/") { + itemPort.Port = strings.Split(port, "/")[0] + itemPort.Protocol = strings.Split(port, "/")[1] + } + itemPort.Strategy = "accept" + datas = append(datas, itemPort) + } + }() + + go func() { + defer wg.Done() + stdout1, err := cmd.Exec("firewall-cmd --zone=public --list-rich-rules") + if err != nil { + return + } + rules := strings.Split(stdout1, "\n") + for _, rule := range rules { + if len(rule) == 0 { + continue + } + itemRule := f.loadInfo(rule) + if len(itemRule.Port) != 0 && (itemRule.Family == "ipv4" || (itemRule.Family == "ipv6" && len(itemRule.Address) != 0)) { + datas = append(datas, itemRule) + } + } + }() + wg.Wait() + return datas, nil +} + +func (f *Firewall) ListForward() ([]FireInfo, error) { + stdout, err := cmd.Exec("firewall-cmd --zone=public --list-forward-ports") + if err != nil { + return nil, err + } + var datas []FireInfo + for _, line := range strings.Split(stdout, "\n") { + line = strings.TrimFunc(line, func(r rune) bool { + return r <= 32 + }) + if ForwardListRegex.MatchString(line) { + match := ForwardListRegex.FindStringSubmatch(line) + if len(match) < 4 { + continue + } + if len(match[4]) == 0 { + match[4] = "127.0.0.1" + } + datas = append(datas, FireInfo{ + Port: match[1], + Protocol: match[2], + TargetIP: match[4], + TargetPort: match[3], + }) + } + } + return datas, nil +} + +func (f *Firewall) ListAddress() ([]FireInfo, error) { + stdout, err := cmd.Exec("firewall-cmd --zone=public --list-rich-rules") + if err != nil { + return nil, err + } + var datas []FireInfo + rules := strings.Split(stdout, "\n") + for _, rule := range rules { + if len(rule) == 0 { + continue + } + itemRule := f.loadInfo(rule) + if len(itemRule.Port) == 0 && len(itemRule.Address) != 0 { + datas = append(datas, itemRule) + } + } + return datas, nil +} + +func (f *Firewall) Port(port FireInfo, operation string) error { + if cmd.CheckIllegal(operation, port.Protocol, port.Port) { + return buserr.New(constant.ErrCmdIllegal) + } + + stdout, err := cmd.Execf("firewall-cmd --zone=public --%s-port=%s/%s --permanent", operation, port.Port, port.Protocol) + if err != nil { + return fmt.Errorf("%s (port: %s/%s strategy: %s) failed, err: %s", operation, port.Port, port.Protocol, port.Strategy, stdout) + } + return nil +} + +func (f *Firewall) RichRules(rule FireInfo, operation string) error { + if cmd.CheckIllegal(operation, rule.Address, rule.Protocol, rule.Port, rule.Strategy) { + return buserr.New(constant.ErrCmdIllegal) + } + ruleStr := "rule family=ipv4 " + if strings.Contains(rule.Address, ":") { + ruleStr = "rule family=ipv6 " + } + if len(rule.Address) != 0 { + ruleStr += fmt.Sprintf("source address=%s ", rule.Address) + } + if len(rule.Port) != 0 { + ruleStr += fmt.Sprintf("port port=%s ", rule.Port) + } + if len(rule.Protocol) != 0 { + ruleStr += fmt.Sprintf("protocol=%s ", rule.Protocol) + } + ruleStr += rule.Strategy + stdout, err := cmd.Execf("firewall-cmd --zone=public --%s-rich-rule '%s' --permanent", operation, ruleStr) + if err != nil { + return fmt.Errorf("%s rich rules (%s) failed, err: %s", operation, ruleStr, stdout) + } + if len(rule.Address) == 0 { + stdout1, err := cmd.Execf("firewall-cmd --zone=public --%s-rich-rule '%s' --permanent", operation, strings.ReplaceAll(ruleStr, "family=ipv4 ", "family=ipv6 ")) + if err != nil { + return fmt.Errorf("%s rich rules (%s) failed, err: %s", operation, strings.ReplaceAll(ruleStr, "family=ipv4 ", "family=ipv6 "), stdout1) + } + } + return nil +} + +func (f *Firewall) PortForward(info Forward, operation string) error { + ruleStr := fmt.Sprintf("firewall-cmd --zone=public --%s-forward-port=port=%s:proto=%s:toport=%s --permanent", operation, info.Port, info.Protocol, info.TargetPort) + if info.TargetIP != "" && info.TargetIP != "127.0.0.1" && info.TargetIP != "localhost" { + ruleStr = fmt.Sprintf("firewall-cmd --zone=public --%s-forward-port=port=%s:proto=%s:toaddr=%s:toport=%s --permanent", operation, info.Port, info.Protocol, info.TargetIP, info.TargetPort) + } + + stdout, err := cmd.Exec(ruleStr) + if err != nil { + return fmt.Errorf("%s port forward failed, err: %s", operation, stdout) + } + if err = f.Reload(); err != nil { + return err + } + return nil +} + +func (f *Firewall) loadInfo(line string) FireInfo { + var itemRule FireInfo + ruleInfo := strings.Split(strings.ReplaceAll(line, "\"", ""), " ") + for _, item := range ruleInfo { + switch { + case strings.Contains(item, "family="): + itemRule.Family = strings.ReplaceAll(item, "family=", "") + case strings.Contains(item, "ipset="): + itemRule.Address = strings.ReplaceAll(item, "ipset=", "") + case strings.Contains(item, "address="): + itemRule.Address = strings.ReplaceAll(item, "address=", "") + case strings.Contains(item, "port="): + itemRule.Port = strings.ReplaceAll(item, "port=", "") + case strings.Contains(item, "protocol="): + itemRule.Protocol = strings.ReplaceAll(item, "protocol=", "") + case item == "accept" || item == "drop" || item == "reject": + itemRule.Strategy = item + } + } + return itemRule +} + +func (f *Firewall) EnableForward() error { + stdout, err := cmd.Exec("firewall-cmd --zone=public --query-masquerade") + if err != nil { + if strings.HasSuffix(strings.TrimSpace(stdout), "no") { + stdout, err = cmd.Exec("firewall-cmd --zone=public --add-masquerade --permanent") + if err != nil { + return fmt.Errorf("%s: %s", err, stdout) + } + return f.Reload() + } + return fmt.Errorf("%s: %s", err, stdout) + } + + return nil +} diff --git a/agent/utils/firewall/client/info.go b/agent/utils/firewall/client/info.go new file mode 100644 index 000000000..3503c25ed --- /dev/null +++ b/agent/utils/firewall/client/info.go @@ -0,0 +1,35 @@ +package client + +type FireInfo struct { + Family string `json:"family"` // ipv4 ipv6 + Address string `json:"address"` // Anywhere + Port string `json:"port"` + Protocol string `json:"protocol"` // tcp udp tcp/udp + Strategy string `json:"strategy"` // accept drop + + Num string `json:"num"` + TargetIP string `json:"targetIP"` + TargetPort string `json:"targetPort"` + + UsedStatus string `json:"usedStatus"` + Description string `json:"description"` +} + +type Forward struct { + Num string `json:"num"` + Protocol string `json:"protocol"` + Port string `json:"port"` + TargetIP string `json:"targetIP"` + TargetPort string `json:"targetPort"` +} + +type IptablesNatInfo struct { + Num string `json:"num"` + Target string `json:"target"` + Protocol string `json:"protocol"` + Opt string `json:"opt"` + Source string `json:"source"` + Destination string `json:"destination"` + SrcPort string `json:"srcPort"` + DestPort string `json:"destPort"` +} diff --git a/agent/utils/firewall/client/iptables.go b/agent/utils/firewall/client/iptables.go new file mode 100644 index 000000000..5ea38a938 --- /dev/null +++ b/agent/utils/firewall/client/iptables.go @@ -0,0 +1,146 @@ +package client + +import ( + "fmt" + "regexp" + "strings" + + "github.com/1Panel-dev/1Panel/agent/app/model" + "github.com/1Panel-dev/1Panel/agent/global" + "github.com/1Panel-dev/1Panel/agent/utils/cmd" +) + +const NatChain = "1PANEL" + +var NatListRegex = regexp.MustCompile(`^(\d+)\s+(.+?)\s+(.+?)\s+(.+?)\s+(.+?)\s+(.+?)(?:\s+(.+?) .+?:(\d{1,5}(?::\d+)?).+?[ :](.+-.+|(?:.+:)?\d{1,5}(?:-\d{1,5})?))?$`) + +type Iptables struct { + CmdStr string +} + +func NewIptables() (*Iptables, error) { + iptables := new(Iptables) + if cmd.HasNoPasswordSudo() { + iptables.CmdStr = "sudo" + } + + return iptables, nil +} + +func (iptables *Iptables) runf(rule string, a ...any) error { + stdout, err := cmd.Execf("%s iptables -t nat %s", iptables.CmdStr, fmt.Sprintf(rule, a...)) + if err != nil { + return fmt.Errorf("%s, %s", err, stdout) + } + if stdout != "" { + return fmt.Errorf("iptables error: %s", stdout) + } + + return nil +} + +func (iptables *Iptables) Check() error { + stdout, err := cmd.Exec("cat /proc/sys/net/ipv4/ip_forward") + if err != nil { + return fmt.Errorf("%s, %s", err, stdout) + } + if stdout == "0" { + return fmt.Errorf("disable") + } + + return nil +} + +func (iptables *Iptables) NatNewChain() error { + return iptables.runf("-N %s", NatChain) +} + +func (iptables *Iptables) NatAppendChain() error { + return iptables.runf("-A PREROUTING -j %s", NatChain) +} + +func (iptables *Iptables) NatList(chain ...string) ([]IptablesNatInfo, error) { + rule := fmt.Sprintf("%s iptables -t nat -nL %s --line", iptables.CmdStr, NatChain) + if len(chain) == 1 { + rule = fmt.Sprintf("%s iptables -t nat -nL %s --line", iptables.CmdStr, chain[0]) + } + stdout, err := cmd.Exec(rule) + if err != nil { + return nil, err + } + + var forwardList []IptablesNatInfo + for _, line := range strings.Split(stdout, "\n") { + line = strings.TrimFunc(line, func(r rune) bool { + return r <= 32 + }) + if NatListRegex.MatchString(line) { + match := NatListRegex.FindStringSubmatch(line) + if !strings.Contains(match[9], ":") { + match[9] = fmt.Sprintf(":%s", match[9]) + } + forwardList = append(forwardList, IptablesNatInfo{ + Num: match[1], + Target: match[2], + Protocol: match[7], + Opt: match[4], + Source: match[5], + Destination: match[6], + SrcPort: match[8], + DestPort: match[9], + }) + } + } + + return forwardList, nil +} + +func (iptables *Iptables) NatAdd(protocol, src, destIp, destPort string, save bool) error { + rule := fmt.Sprintf("-A %s -p %s --dport %s -j REDIRECT --to-port %s", NatChain, protocol, src, destPort) + if destIp != "" && destIp != "127.0.0.1" && destIp != "localhost" { + rule = fmt.Sprintf("-A %s -p %s --dport %s -j DNAT --to-destination %s:%s", NatChain, protocol, src, destIp, destPort) + } + if err := iptables.runf(rule); err != nil { + return err + } + + if save { + return global.DB.Save(&model.Forward{ + Protocol: protocol, + Port: src, + TargetIP: destIp, + TargetPort: destPort, + }).Error + } + return nil +} + +func (iptables *Iptables) NatRemove(num string, protocol, src, destIp, destPort string) error { + if err := iptables.runf("-D %s %s", NatChain, num); err != nil { + return err + } + + global.DB.Where( + "protocol = ? AND port = ? AND target_ip = ? AND target_port = ?", + protocol, + src, + destIp, + destPort, + ).Delete(&model.Forward{}) + return nil +} + +func (iptables *Iptables) Reload() error { + if err := iptables.runf("-F %s", NatChain); err != nil { + return err + } + + var rules []model.Forward + global.DB.Find(&rules) + for _, forward := range rules { + if err := iptables.NatAdd(forward.Protocol, forward.Port, forward.TargetIP, forward.TargetPort, false); err != nil { + return err + } + } + return nil +} diff --git a/agent/utils/firewall/client/ufw.go b/agent/utils/firewall/client/ufw.go new file mode 100644 index 000000000..ecc5a1869 --- /dev/null +++ b/agent/utils/firewall/client/ufw.go @@ -0,0 +1,322 @@ +package client + +import ( + "fmt" + "strings" + + "github.com/1Panel-dev/1Panel/agent/buserr" + "github.com/1Panel-dev/1Panel/agent/constant" + "github.com/1Panel-dev/1Panel/agent/utils/cmd" +) + +type Ufw struct { + CmdStr string +} + +func NewUfw() (*Ufw, error) { + var ufw Ufw + if cmd.HasNoPasswordSudo() { + ufw.CmdStr = "sudo ufw" + } else { + ufw.CmdStr = "ufw" + } + return &ufw, nil +} + +func (f *Ufw) Name() string { + return "ufw" +} + +func (f *Ufw) Status() (string, error) { + stdout, _ := cmd.Execf("%s status | grep Status", f.CmdStr) + if stdout == "Status: active\n" { + return "running", nil + } + stdout1, _ := cmd.Execf("%s status | grep 状态", f.CmdStr) + if stdout1 == "状态: 激活\n" { + return "running", nil + } + return "not running", nil +} + +func (f *Ufw) Version() (string, error) { + stdout, err := cmd.Execf("%s version | grep ufw", f.CmdStr) + if err != nil { + return "", fmt.Errorf("load the firewall status failed, err: %s", stdout) + } + info := strings.ReplaceAll(stdout, "\n", "") + return strings.ReplaceAll(info, "ufw ", ""), nil +} + +func (f *Ufw) Start() error { + stdout, err := cmd.Execf("echo y | %s enable", f.CmdStr) + if err != nil { + return fmt.Errorf("enable the firewall failed, err: %s", stdout) + } + return nil +} + +func (f *Ufw) Stop() error { + stdout, err := cmd.Execf("%s disable", f.CmdStr) + if err != nil { + return fmt.Errorf("stop the firewall failed, err: %s", stdout) + } + return nil +} + +func (f *Ufw) Restart() error { + if err := f.Stop(); err != nil { + return err + } + if err := f.Start(); err != nil { + return err + } + return nil +} + +func (f *Ufw) Reload() error { + return nil +} + +func (f *Ufw) ListPort() ([]FireInfo, error) { + stdout, err := cmd.Execf("%s status verbose", f.CmdStr) + if err != nil { + return nil, err + } + portInfos := strings.Split(stdout, "\n") + var datas []FireInfo + isStart := false + for _, line := range portInfos { + if strings.HasPrefix(line, "-") { + isStart = true + continue + } + if !isStart { + continue + } + itemFire := f.loadInfo(line, "port") + if len(itemFire.Port) != 0 && itemFire.Port != "Anywhere" && !strings.Contains(itemFire.Port, ".") { + itemFire.Port = strings.ReplaceAll(itemFire.Port, ":", "-") + datas = append(datas, itemFire) + } + } + return datas, nil +} + +func (f *Ufw) ListForward() ([]FireInfo, error) { + iptables, err := NewIptables() + if err != nil { + return nil, err + } + rules, err := iptables.NatList() + if err != nil { + return nil, err + } + + var list []FireInfo + for _, rule := range rules { + dest := strings.Split(rule.DestPort, ":") + if len(dest) < 2 { + continue + } + if len(dest[0]) == 0 { + dest[0] = "127.0.0.1" + } + list = append(list, FireInfo{ + Num: rule.Num, + Protocol: rule.Protocol, + Port: rule.SrcPort, + TargetIP: dest[0], + TargetPort: dest[1], + }) + } + return list, nil +} + +func (f *Ufw) ListAddress() ([]FireInfo, error) { + stdout, err := cmd.Execf("%s status verbose", f.CmdStr) + if err != nil { + return nil, err + } + portInfos := strings.Split(stdout, "\n") + var datas []FireInfo + isStart := false + for _, line := range portInfos { + if strings.HasPrefix(line, "-") { + isStart = true + continue + } + if !isStart { + continue + } + if !strings.Contains(line, " IN") { + continue + } + itemFire := f.loadInfo(line, "address") + if strings.Contains(itemFire.Port, ".") { + itemFire.Address += ("-" + itemFire.Port) + itemFire.Port = "" + } + if len(itemFire.Port) == 0 && len(itemFire.Address) != 0 { + datas = append(datas, itemFire) + } + } + return datas, nil +} + +func (f *Ufw) Port(port FireInfo, operation string) error { + switch port.Strategy { + case "accept": + port.Strategy = "allow" + case "drop": + port.Strategy = "deny" + default: + return fmt.Errorf("unsupported strategy %s", port.Strategy) + } + if cmd.CheckIllegal(port.Protocol, port.Port) { + return buserr.New(constant.ErrCmdIllegal) + } + + command := fmt.Sprintf("%s %s %s", f.CmdStr, port.Strategy, port.Port) + if operation == "remove" { + command = fmt.Sprintf("%s delete %s %s", f.CmdStr, port.Strategy, port.Port) + } + if len(port.Protocol) != 0 { + command += fmt.Sprintf("/%s", port.Protocol) + } + stdout, err := cmd.Exec(command) + if err != nil { + return fmt.Errorf("%s (%s) failed, err: %s", operation, command, stdout) + } + return nil +} + +func (f *Ufw) RichRules(rule FireInfo, operation string) error { + switch rule.Strategy { + case "accept": + rule.Strategy = "allow" + case "drop": + rule.Strategy = "deny" + default: + return fmt.Errorf("unsupported strategy %s", rule.Strategy) + } + + if cmd.CheckIllegal(operation, rule.Protocol, rule.Address, rule.Port) { + return buserr.New(constant.ErrCmdIllegal) + } + + ruleStr := fmt.Sprintf("%s insert 1 %s ", f.CmdStr, rule.Strategy) + if operation == "remove" { + ruleStr = fmt.Sprintf("%s delete %s ", f.CmdStr, rule.Strategy) + } + if len(rule.Protocol) != 0 { + ruleStr += fmt.Sprintf("proto %s ", rule.Protocol) + } + if strings.Contains(rule.Address, "-") { + ruleStr += fmt.Sprintf("from %s to %s ", strings.Split(rule.Address, "-")[0], strings.Split(rule.Address, "-")[1]) + } else { + ruleStr += fmt.Sprintf("from %s ", rule.Address) + } + if len(rule.Port) != 0 { + ruleStr += fmt.Sprintf("to any port %s ", rule.Port) + } + + stdout, err := cmd.Exec(ruleStr) + if err != nil { + if strings.Contains(stdout, "ERROR: Invalid position") || strings.Contains(stdout, "ERROR: 无效位置") { + stdout, err := cmd.Exec(strings.ReplaceAll(ruleStr, "insert 1 ", "")) + if err != nil { + return fmt.Errorf("%s rich rules (%s), failed, err: %s", operation, ruleStr, stdout) + } + return nil + } + return fmt.Errorf("%s rich rules (%s), failed, err: %s", operation, ruleStr, stdout) + } + return nil +} + +func (f *Ufw) PortForward(info Forward, operation string) error { + iptables, err := NewIptables() + if err != nil { + return err + } + + if operation == "add" { + err = iptables.NatAdd(info.Protocol, info.Port, info.TargetIP, info.TargetPort, true) + } else { + err = iptables.NatRemove(info.Num, info.Protocol, info.Port, info.TargetIP, info.TargetPort) + } + if err != nil { + return fmt.Errorf("%s port forward failed, err: %s", operation, err) + } + return nil +} + +func (f *Ufw) loadInfo(line string, fireType string) FireInfo { + fields := strings.Fields(line) + var itemInfo FireInfo + if strings.Contains(line, "LIMIT") || strings.Contains(line, "ALLOW FWD") { + return itemInfo + } + if len(fields) < 4 { + return itemInfo + } + if fields[1] == "(v6)" && fireType == "port" { + return itemInfo + } + if fields[0] == "Anywhere" && fireType != "port" { + itemInfo.Strategy = "drop" + if fields[1] == "ALLOW" { + itemInfo.Strategy = "accept" + } + if fields[1] == "(v6)" { + if fields[2] == "ALLOW" { + itemInfo.Strategy = "accept" + } + itemInfo.Address = fields[4] + } else { + itemInfo.Address = fields[3] + } + return itemInfo + } + if strings.Contains(fields[0], "/") { + itemInfo.Port = strings.Split(fields[0], "/")[0] + itemInfo.Protocol = strings.Split(fields[0], "/")[1] + } else { + itemInfo.Port = fields[0] + itemInfo.Protocol = "tcp/udp" + } + itemInfo.Family = "ipv4" + if fields[1] == "ALLOW" { + itemInfo.Strategy = "accept" + } else { + itemInfo.Strategy = "drop" + } + itemInfo.Address = fields[3] + + return itemInfo +} + +func (f *Ufw) EnableForward() error { + iptables, err := NewIptables() + if err != nil { + return err + } + _ = iptables.NatNewChain() + + rules, err := iptables.NatList("PREROUTING") + if err != nil { + return err + } + for _, rule := range rules { + if rule.Target == NatChain { + goto reload + } + } + + if err = iptables.NatAppendChain(); err != nil { + return err + } +reload: + return iptables.Reload() +} diff --git a/agent/utils/http/get.go b/agent/utils/http/get.go new file mode 100644 index 000000000..692c648cb --- /dev/null +++ b/agent/utils/http/get.go @@ -0,0 +1,42 @@ +package http + +import ( + "context" + "errors" + "net/http" + "strings" + "time" + + "github.com/1Panel-dev/1Panel/agent/buserr" + "github.com/1Panel-dev/1Panel/agent/utils/xpack" +) + +func GetHttpRes(url string) (*http.Response, error) { + client := &http.Client{ + Timeout: time.Second * 300, + } + transport := xpack.LoadRequestTransport() + client.Transport = transport + + req, err := http.NewRequestWithContext(context.Background(), "GET", url, nil) + if err != nil { + return nil, buserr.WithMap("ErrCreateHttpClient", map[string]interface{}{"err": err.Error()}, err) + } + + resp, err := client.Do(req) + if err != nil { + if errors.Is(err, context.DeadlineExceeded) { + return nil, buserr.WithMap("ErrHttpReqTimeOut", map[string]interface{}{"err": err.Error()}, err) + } else { + if strings.Contains(err.Error(), "no such host") { + return nil, buserr.New("ErrNoSuchHost") + } + return nil, buserr.WithMap("ErrHttpReqFailed", map[string]interface{}{"err": err.Error()}, err) + } + } + if resp.StatusCode == 404 { + return nil, buserr.New("ErrHttpReqNotFound") + } + + return resp, nil +} diff --git a/agent/utils/http/request.go b/agent/utils/http/request.go new file mode 100644 index 000000000..b3c56bee0 --- /dev/null +++ b/agent/utils/http/request.go @@ -0,0 +1,45 @@ +package http + +import ( + "context" + "io" + "net/http" + "time" + + "github.com/1Panel-dev/1Panel/agent/global" + "github.com/1Panel-dev/1Panel/agent/utils/xpack" +) + +func HandleGet(url, method string, timeout int) (int, []byte, error) { + transport := xpack.LoadRequestTransport() + return HandleGetWithTransport(url, method, transport, timeout) +} + +func HandleGetWithTransport(url, method string, transport *http.Transport, timeout int) (int, []byte, error) { + defer func() { + if r := recover(); r != nil { + global.LOG.Errorf("handle request failed, error message: %v", r) + return + } + }() + + client := http.Client{Timeout: time.Duration(timeout) * time.Second, Transport: transport} + ctx, cancel := context.WithTimeout(context.Background(), time.Duration(timeout)*time.Second) + defer cancel() + request, err := http.NewRequestWithContext(ctx, method, url, nil) + if err != nil { + return 0, nil, err + } + request.Header.Set("Content-Type", "application/json") + resp, err := client.Do(request) + if err != nil { + return 0, nil, err + } + body, err := io.ReadAll(resp.Body) + if err != nil { + return 0, nil, err + } + defer resp.Body.Close() + + return resp.StatusCode, body, nil +} diff --git a/agent/utils/ini_conf/ini.go b/agent/utils/ini_conf/ini.go new file mode 100644 index 000000000..4ea80a2a6 --- /dev/null +++ b/agent/utils/ini_conf/ini.go @@ -0,0 +1,19 @@ +package ini_conf + +import "gopkg.in/ini.v1" + +func GetIniValue(filePath, Group, Key string) (string, error) { + cfg, err := ini.Load(filePath) + if err != nil { + return "", err + } + service, err := cfg.GetSection(Group) + if err != nil { + return "", err + } + startKey, err := service.GetKey(Key) + if err != nil { + return "", err + } + return startKey.Value(), nil +} diff --git a/agent/utils/jwt/jwt.go b/agent/utils/jwt/jwt.go new file mode 100644 index 000000000..c2ecb4517 --- /dev/null +++ b/agent/utils/jwt/jwt.go @@ -0,0 +1,69 @@ +package jwt + +import ( + "time" + + "github.com/1Panel-dev/1Panel/agent/app/repo" + "github.com/1Panel-dev/1Panel/agent/constant" + + "github.com/golang-jwt/jwt/v4" +) + +type JWT struct { + SigningKey []byte +} + +type JwtRequest struct { + BaseClaims + BufferTime int64 + jwt.RegisteredClaims +} + +type CustomClaims struct { + BaseClaims + BufferTime int64 + jwt.RegisteredClaims +} + +type BaseClaims struct { + ID uint + Name string +} + +func NewJWT() *JWT { + settingRepo := repo.NewISettingRepo() + jwtSign, _ := settingRepo.Get(settingRepo.WithByKey("JWTSigningKey")) + return &JWT{ + []byte(jwtSign.Value), + } +} + +func (j *JWT) CreateClaims(baseClaims BaseClaims) CustomClaims { + claims := CustomClaims{ + BaseClaims: baseClaims, + BufferTime: constant.JWTBufferTime, + RegisteredClaims: jwt.RegisteredClaims{ + ExpiresAt: jwt.NewNumericDate(time.Now().Add(time.Second * time.Duration(constant.JWTBufferTime))), + Issuer: constant.JWTIssuer, + }, + } + return claims +} + +func (j *JWT) CreateToken(request CustomClaims) (string, error) { + token := jwt.NewWithClaims(jwt.SigningMethodHS256, &request) + return token.SignedString(j.SigningKey) +} + +func (j *JWT) ParseToken(tokenStr string) (*JwtRequest, error) { + token, err := jwt.ParseWithClaims(tokenStr, &JwtRequest{}, func(token *jwt.Token) (interface{}, error) { + return j.SigningKey, nil + }) + if err != nil || token == nil { + return nil, constant.ErrTokenParse + } + if claims, ok := token.Claims.(*JwtRequest); ok && token.Valid { + return claims, nil + } + return nil, constant.ErrTokenParse +} diff --git a/agent/utils/mysql/client.go b/agent/utils/mysql/client.go new file mode 100644 index 000000000..64bcfe5a2 --- /dev/null +++ b/agent/utils/mysql/client.go @@ -0,0 +1,77 @@ +package mysql + +import ( + "context" + "database/sql" + "errors" + "fmt" + "strings" + "time" + + "github.com/1Panel-dev/1Panel/agent/buserr" + "github.com/1Panel-dev/1Panel/agent/constant" + "github.com/1Panel-dev/1Panel/agent/global" + "github.com/1Panel-dev/1Panel/agent/utils/mysql/client" +) + +type MysqlClient interface { + Create(info client.CreateInfo) error + CreateUser(info client.CreateInfo, withDeleteDB bool) error + Delete(info client.DeleteInfo) error + + ChangePassword(info client.PasswordChangeInfo) error + ChangeAccess(info client.AccessChangeInfo) error + + Backup(info client.BackupInfo) error + Recover(info client.RecoverInfo) error + + SyncDB(version string) ([]client.SyncDBInfo, error) + Close() +} + +func NewMysqlClient(conn client.DBInfo) (MysqlClient, error) { + if conn.From == "local" { + connArgs := []string{"exec", conn.Address, conn.Type, "-u" + conn.Username, "-p" + conn.Password, "-e"} + return client.NewLocal(connArgs, conn.Type, conn.Address, conn.Password, conn.Database), nil + } + + if strings.Contains(conn.Address, ":") { + conn.Address = fmt.Sprintf("[%s]", conn.Address) + } + + tlsItem, err := client.ConnWithSSL(conn.SSL, conn.SkipVerify, conn.ClientKey, conn.ClientCert, conn.RootCert) + if err != nil { + return nil, err + } + connArgs := fmt.Sprintf("%s:%s@tcp(%s:%d)/?charset=utf8%s", conn.Username, conn.Password, conn.Address, conn.Port, tlsItem) + db, err := sql.Open("mysql", connArgs) + if err != nil { + return nil, err + } + + ctx, cancel := context.WithTimeout(context.Background(), time.Duration(conn.Timeout)*time.Second) + defer cancel() + if err := db.PingContext(ctx); err != nil { + global.LOG.Errorf("test mysql conn failed, err: %v", err) + return nil, err + } + if errors.Is(ctx.Err(), context.DeadlineExceeded) { + return nil, buserr.New(constant.ErrExecTimeOut) + } + + return client.NewRemote(client.Remote{ + Type: conn.Type, + Client: db, + Database: conn.Database, + User: conn.Username, + Password: conn.Password, + Address: conn.Address, + Port: conn.Port, + + SSL: conn.SSL, + RootCert: conn.RootCert, + ClientKey: conn.ClientKey, + ClientCert: conn.ClientCert, + SkipVerify: conn.SkipVerify, + }), nil +} diff --git a/agent/utils/mysql/client/info.go b/agent/utils/mysql/client/info.go new file mode 100644 index 000000000..4564e7d8e --- /dev/null +++ b/agent/utils/mysql/client/info.go @@ -0,0 +1,137 @@ +package client + +import ( + "crypto/tls" + "crypto/x509" + "errors" + + "github.com/1Panel-dev/1Panel/agent/global" + "github.com/go-sql-driver/mysql" +) + +type DBInfo struct { + Type string `json:"type"` + From string `json:"from"` + Database string `json:"database"` + Address string `json:"address"` + Port uint `json:"port"` + Username string `json:"userName"` + Password string `json:"password"` + + SSL bool `json:"ssl"` + RootCert string `json:"rootCert"` + ClientKey string `json:"clientKey"` + ClientCert string `json:"clientCert"` + SkipVerify bool `json:"skipVerify"` + + Timeout uint `json:"timeout"` // second +} + +type CreateInfo struct { + Name string `json:"name"` + Format string `json:"format"` + Version string `json:"version"` + Username string `json:"userName"` + Password string `json:"password"` + Permission string `json:"permission"` + + Timeout uint `json:"timeout"` // second +} + +type DeleteInfo struct { + Name string `json:"name"` + Version string `json:"version"` + Username string `json:"userName"` + Permission string `json:"permission"` + + ForceDelete bool `json:"forceDelete"` + Timeout uint `json:"timeout"` // second +} + +type PasswordChangeInfo struct { + Name string `json:"name"` + Version string `json:"version"` + Username string `json:"userName"` + Password string `json:"password"` + Permission string `json:"permission"` + + Timeout uint `json:"timeout"` // second +} + +type AccessChangeInfo struct { + Name string `json:"name"` + Version string `json:"version"` + Username string `json:"userName"` + Password string `json:"password"` + OldPermission string `json:"oldPermission"` + Permission string `json:"permission"` + + Timeout uint `json:"timeout"` // second +} + +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"` + + Timeout uint `json:"timeout"` // second +} + +type RecoverInfo struct { + Name string `json:"name"` + Type string `json:"type"` + Version string `json:"version"` + Format string `json:"format"` + SourceFile string `json:"sourceFile"` + + Timeout uint `json:"timeout"` // second +} + +type SyncDBInfo struct { + Name string `json:"name"` + From string `json:"from"` + MysqlName string `json:"mysqlName"` + Format string `json:"format"` + Username string `json:"username"` + Password string `json:"password"` + Permission string `json:"permission"` +} + +var formatMap = map[string]string{ + "utf8": "utf8_general_ci", + "utf8mb4": "utf8mb4_general_ci", + "gbk": "gbk_chinese_ci", + "big5": "big5_chinese_ci", +} + +func ConnWithSSL(ssl, skipVerify bool, clientKey, clientCert, rootCert string) (string, error) { + if !ssl { + return "", nil + } + tlsConfig := &tls.Config{ + InsecureSkipVerify: skipVerify, + } + if len(rootCert) != 0 { + pool := x509.NewCertPool() + if ok := pool.AppendCertsFromPEM([]byte(rootCert)); !ok { + global.LOG.Error("append certs from pem failed") + return "", errors.New("unable to append root cert to pool") + } + tlsConfig.RootCAs = pool + } + if len(clientCert) != 0 && len(clientKey) != 0 { + cert, err := tls.X509KeyPair([]byte(clientCert), []byte(clientKey)) + if err != nil { + return "", err + } + tlsConfig.Certificates = []tls.Certificate{cert} + } + if err := mysql.RegisterTLSConfig("cloudsql", tlsConfig); err != nil { + global.LOG.Errorf("register tls config failed, err: %v", err) + return "", err + } + return "&tls=cloudsql", nil +} diff --git a/agent/utils/mysql/client/local.go b/agent/utils/mysql/client/local.go new file mode 100644 index 000000000..e0fd9d1c6 --- /dev/null +++ b/agent/utils/mysql/client/local.go @@ -0,0 +1,381 @@ +package client + +import ( + "bytes" + "compress/gzip" + "context" + "errors" + "fmt" + "os" + "os/exec" + "path" + "strings" + "time" + + "github.com/1Panel-dev/1Panel/agent/buserr" + "github.com/1Panel-dev/1Panel/agent/constant" + "github.com/1Panel-dev/1Panel/agent/global" + "github.com/1Panel-dev/1Panel/agent/utils/files" +) + +type Local struct { + Type string + PrefixCommand []string + Database string + Password string + ContainerName string +} + +func NewLocal(command []string, dbType, containerName, password, database string) *Local { + return &Local{Type: dbType, PrefixCommand: command, ContainerName: containerName, Password: password, Database: database} +} + +func (r *Local) Create(info CreateInfo) error { + createSql := fmt.Sprintf("create database `%s` default character set %s collate %s", info.Name, info.Format, formatMap[info.Format]) + if err := r.ExecSQL(createSql, info.Timeout); err != nil { + if strings.Contains(strings.ToLower(err.Error()), "error 1007") { + return buserr.New(constant.ErrDatabaseIsExist) + } + return err + } + + if err := r.CreateUser(info, true); err != nil { + _ = r.ExecSQL(fmt.Sprintf("drop database if exists `%s`", info.Name), info.Timeout) + return err + } + + return nil +} + +func (r *Local) CreateUser(info CreateInfo, withDeleteDB bool) error { + var userlist []string + if strings.Contains(info.Permission, ",") { + ips := strings.Split(info.Permission, ",") + for _, ip := range ips { + if len(ip) != 0 { + userlist = append(userlist, fmt.Sprintf("'%s'@'%s'", info.Username, ip)) + } + } + } else { + userlist = append(userlist, fmt.Sprintf("'%s'@'%s'", info.Username, info.Permission)) + } + + for _, user := range userlist { + if err := r.ExecSQL(fmt.Sprintf("create user %s identified by '%s';", user, info.Password), info.Timeout); err != nil { + if strings.Contains(strings.ToLower(err.Error()), "error 1396") { + return buserr.New(constant.ErrUserIsExist) + } + if withDeleteDB { + _ = r.Delete(DeleteInfo{ + Name: info.Name, + Version: info.Version, + Username: info.Username, + Permission: info.Permission, + ForceDelete: true, + Timeout: 300}) + } + return err + } + grantStr := fmt.Sprintf("grant all privileges on `%s`.* to %s", info.Name, user) + if info.Name == "*" { + grantStr = fmt.Sprintf("grant all privileges on *.* to %s", user) + } + if strings.HasPrefix(info.Version, "5.7") || strings.HasPrefix(info.Version, "5.6") { + grantStr = fmt.Sprintf("%s identified by '%s' with grant option;", grantStr, info.Password) + } else { + grantStr = grantStr + " with grant option;" + } + if err := r.ExecSQL(grantStr, info.Timeout); err != nil { + if withDeleteDB { + _ = r.Delete(DeleteInfo{ + Name: info.Name, + Version: info.Version, + Username: info.Username, + Permission: info.Permission, + ForceDelete: true, + Timeout: 300}) + } + return err + } + } + return nil +} + +func (r *Local) Delete(info DeleteInfo) error { + var userlist []string + if strings.Contains(info.Permission, ",") { + ips := strings.Split(info.Permission, ",") + for _, ip := range ips { + if len(ip) != 0 { + userlist = append(userlist, fmt.Sprintf("'%s'@'%s'", info.Username, ip)) + } + } + } else { + userlist = append(userlist, fmt.Sprintf("'%s'@'%s'", info.Username, info.Permission)) + } + + for _, user := range userlist { + if strings.HasPrefix(info.Version, "5.6") { + if err := r.ExecSQL(fmt.Sprintf("drop user %s", user), info.Timeout); err != nil && !info.ForceDelete { + return err + } + } else { + if err := r.ExecSQL(fmt.Sprintf("drop user if exists %s", user), info.Timeout); err != nil && !info.ForceDelete { + return err + } + } + } + if len(info.Name) != 0 { + if err := r.ExecSQL(fmt.Sprintf("drop database if exists `%s`", info.Name), info.Timeout); err != nil && !info.ForceDelete { + return err + } + } + if !info.ForceDelete { + global.LOG.Info("execute delete database sql successful, now start to drop uploads and records") + } + + return nil +} + +func (r *Local) ChangePassword(info PasswordChangeInfo) error { + if info.Username != "root" { + var userlist []string + if strings.Contains(info.Permission, ",") { + ips := strings.Split(info.Permission, ",") + for _, ip := range ips { + if len(ip) != 0 { + userlist = append(userlist, fmt.Sprintf("'%s'@'%s'", info.Username, ip)) + } + } + } else { + userlist = append(userlist, fmt.Sprintf("'%s'@'%s'", info.Username, info.Permission)) + } + + for _, user := range userlist { + passwordChangeSql := fmt.Sprintf("set password for %s = password('%s')", user, info.Password) + if !strings.HasPrefix(info.Version, "5.7") && !strings.HasPrefix(info.Version, "5.6") { + passwordChangeSql = fmt.Sprintf("ALTER USER %s IDENTIFIED BY '%s';", user, info.Password) + } + if err := r.ExecSQL(passwordChangeSql, info.Timeout); err != nil { + return err + } + } + return nil + } + + hosts, err := r.ExecSQLForRows("select host from mysql.user where user='root';", info.Timeout) + if err != nil { + return err + } + for _, host := range hosts { + if host == "%" || host == "localhost" { + passwordRootChangeCMD := fmt.Sprintf("set password for 'root'@'%s' = password('%s')", host, info.Password) + if !strings.HasPrefix(info.Version, "5.7") && !strings.HasPrefix(info.Version, "5.6") { + passwordRootChangeCMD = fmt.Sprintf("alter user 'root'@'%s' identified by '%s';", host, info.Password) + } + if err := r.ExecSQL(passwordRootChangeCMD, info.Timeout); err != nil { + return err + } + } + } + + return nil +} + +func (r *Local) ChangeAccess(info AccessChangeInfo) error { + if info.Username == "root" { + info.OldPermission = "%" + info.Name = "*" + info.Password = r.Password + } + if info.Permission != info.OldPermission { + if err := r.Delete(DeleteInfo{ + Version: info.Version, + Username: info.Username, + Permission: info.OldPermission, + ForceDelete: true, + Timeout: 300}); err != nil { + return err + } + if info.Username == "root" { + return nil + } + } + if err := r.CreateUser(CreateInfo{ + Name: info.Name, + Version: info.Version, + Username: info.Username, + Password: info.Password, + Permission: info.Permission, + Timeout: info.Timeout, + }, false); err != nil { + return err + } + if err := r.ExecSQL("flush privileges", 300); err != nil { + return err + } + return nil +} + +func (r *Local) Backup(info BackupInfo) error { + fileOp := files.NewFileOp() + if !fileOp.Stat(info.TargetDir) { + if err := os.MkdirAll(info.TargetDir, os.ModePerm); err != nil { + return fmt.Errorf("mkdir %s failed, err: %v", info.TargetDir, err) + } + } + outfile, err := os.OpenFile(path.Join(info.TargetDir, info.FileName), os.O_RDWR|os.O_CREATE, 0755) + if err != nil { + return fmt.Errorf("open file %s failed, err: %v", path.Join(info.TargetDir, info.FileName), err) + } + defer outfile.Close() + dumpCmd := "mysqldump" + if r.Type == constant.AppMariaDB { + dumpCmd = "mariadb-dump" + } + global.LOG.Infof("start to %s | gzip > %s.gzip", dumpCmd, info.TargetDir+"/"+info.FileName) + cmd := exec.Command("docker", "exec", r.ContainerName, dumpCmd, "-uroot", "-p"+r.Password, "--default-character-set="+info.Format, info.Name) + var stderr bytes.Buffer + cmd.Stderr = &stderr + + gzipCmd := exec.Command("gzip", "-cf") + gzipCmd.Stdin, _ = cmd.StdoutPipe() + gzipCmd.Stdout = outfile + _ = gzipCmd.Start() + + if err := cmd.Run(); err != nil { + return fmt.Errorf("handle backup database failed, err: %v", stderr.String()) + } + _ = gzipCmd.Wait() + return nil +} + +func (r *Local) Recover(info RecoverInfo) error { + fi, _ := os.Open(info.SourceFile) + defer fi.Close() + cmd := exec.Command("docker", "exec", "-i", r.ContainerName, r.Type, "-uroot", "-p"+r.Password, "--default-character-set="+info.Format, info.Name) + 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 +} + +func (r *Local) SyncDB(version string) ([]SyncDBInfo, error) { + var datas []SyncDBInfo + lines, err := r.ExecSQLForRows("SELECT SCHEMA_NAME, DEFAULT_CHARACTER_SET_NAME FROM information_schema.SCHEMATA", 300) + if err != nil { + return datas, err + } + for _, line := range lines { + parts := strings.Fields(line) + if len(parts) != 2 { + continue + } + if parts[0] == "SCHEMA_NAME" || parts[0] == "information_schema" || parts[0] == "mysql" || parts[0] == "performance_schema" || parts[0] == "sys" || parts[0] == "__recycle_bin__" || parts[0] == "recycle_bin" { + continue + } + dataItem := SyncDBInfo{ + Name: parts[0], + From: "local", + MysqlName: r.Database, + Format: parts[1], + } + userLines, err := r.ExecSQLForRows(fmt.Sprintf("select user,host from mysql.db where db = '%s'", parts[0]), 300) + if err != nil { + global.LOG.Debugf("sync user of db %s failed, err: %v", parts[0], err) + dataItem.Permission = "%" + datas = append(datas, dataItem) + continue + } + + var permissionItem []string + isLocal := true + i := 0 + for _, userline := range userLines { + userparts := strings.Fields(userline) + if len(userparts) != 2 { + continue + } + if userparts[0] == "root" { + continue + } + if i == 0 { + dataItem.Username = userparts[0] + } + dataItem.Username = userparts[0] + if dataItem.Username == userparts[0] && userparts[1] == "%" { + isLocal = false + dataItem.Permission = "%" + } else if dataItem.Username == userparts[0] && userparts[1] != "localhost" { + isLocal = false + permissionItem = append(permissionItem, userparts[1]) + } + } + if len(dataItem.Username) == 0 { + dataItem.Permission = "%" + } else { + if isLocal { + dataItem.Permission = "localhost" + } + if len(dataItem.Permission) == 0 { + dataItem.Permission = strings.Join(permissionItem, ",") + } + } + datas = append(datas, dataItem) + } + return datas, nil +} + +func (r *Local) Close() {} + +func (r *Local) ExecSQL(command string, timeout uint) error { + itemCommand := r.PrefixCommand[:] + itemCommand = append(itemCommand, command) + ctx, cancel := context.WithTimeout(context.Background(), time.Duration(timeout)*time.Second) + defer cancel() + cmd := exec.CommandContext(ctx, "docker", itemCommand...) + stdout, err := cmd.CombinedOutput() + if errors.Is(ctx.Err(), context.DeadlineExceeded) { + return buserr.New(constant.ErrExecTimeOut) + } + 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 +} + +func (r *Local) ExecSQLForRows(command string, timeout uint) ([]string, error) { + itemCommand := r.PrefixCommand[:] + itemCommand = append(itemCommand, command) + ctx, cancel := context.WithTimeout(context.Background(), time.Duration(timeout)*time.Second) + defer cancel() + cmd := exec.CommandContext(ctx, "docker", itemCommand...) + stdout, err := cmd.CombinedOutput() + if errors.Is(ctx.Err(), context.DeadlineExceeded) { + return nil, buserr.New(constant.ErrExecTimeOut) + } + 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 nil, errors.New(stdStr) + } + return strings.Split(stdStr, "\n"), nil +} diff --git a/agent/utils/mysql/client/remote.go b/agent/utils/mysql/client/remote.go new file mode 100644 index 000000000..0ccb43479 --- /dev/null +++ b/agent/utils/mysql/client/remote.go @@ -0,0 +1,472 @@ +package client + +import ( + "bytes" + "compress/gzip" + "context" + "database/sql" + "errors" + "fmt" + "os" + "os/exec" + "path" + "strings" + "time" + + "github.com/1Panel-dev/1Panel/agent/buserr" + "github.com/1Panel-dev/1Panel/agent/constant" + "github.com/1Panel-dev/1Panel/agent/global" + "github.com/1Panel-dev/1Panel/agent/utils/files" + "github.com/docker/docker/api/types/image" + "github.com/docker/docker/client" +) + +type Remote struct { + Type string + Client *sql.DB + Database string + User string + Password string + Address string + Port uint + + SSL bool + RootCert string + ClientKey string + ClientCert string + SkipVerify bool +} + +func NewRemote(db Remote) *Remote { + return &db +} + +func (r *Remote) Create(info CreateInfo) error { + createSql := fmt.Sprintf("create database `%s` default character set %s collate %s", info.Name, info.Format, formatMap[info.Format]) + if err := r.ExecSQL(createSql, info.Timeout); err != nil { + if strings.Contains(strings.ToLower(err.Error()), "error 1007") { + return buserr.New(constant.ErrDatabaseIsExist) + } + return err + } + + if err := r.CreateUser(info, true); err != nil { + _ = r.ExecSQL(fmt.Sprintf("drop database if exists `%s`", info.Name), info.Timeout) + return err + } + + return nil +} + +func (r *Remote) CreateUser(info CreateInfo, withDeleteDB bool) error { + var userlist []string + if strings.Contains(info.Permission, ",") { + ips := strings.Split(info.Permission, ",") + for _, ip := range ips { + if len(ip) != 0 { + userlist = append(userlist, fmt.Sprintf("'%s'@'%s'", info.Username, ip)) + } + } + } else { + userlist = append(userlist, fmt.Sprintf("'%s'@'%s'", info.Username, info.Permission)) + } + + for _, user := range userlist { + if err := r.ExecSQL(fmt.Sprintf("create user %s identified by '%s';", user, info.Password), info.Timeout); err != nil { + if strings.Contains(strings.ToLower(err.Error()), "error 1396") { + return buserr.New(constant.ErrUserIsExist) + } + if withDeleteDB { + _ = r.Delete(DeleteInfo{ + Name: info.Name, + Version: info.Version, + Username: info.Username, + Permission: info.Permission, + ForceDelete: true, + Timeout: 300}) + } + return err + } + grantStr := fmt.Sprintf("grant all privileges on `%s`.* to %s", info.Name, user) + if info.Name == "*" { + grantStr = fmt.Sprintf("grant all privileges on *.* to %s", user) + } + if strings.HasPrefix(info.Version, "5.7") || strings.HasPrefix(info.Version, "5.6") { + grantStr = fmt.Sprintf("%s identified by '%s' with grant option;", grantStr, info.Password) + } else { + grantStr = grantStr + " with grant option;" + } + if err := r.ExecSQL(grantStr, info.Timeout); err != nil { + if withDeleteDB { + _ = r.Delete(DeleteInfo{ + Name: info.Name, + Version: info.Version, + Username: info.Username, + Permission: info.Permission, + ForceDelete: true, + Timeout: 300}) + } + return err + } + } + return nil +} + +func (r *Remote) Delete(info DeleteInfo) error { + var userlist []string + if strings.Contains(info.Permission, ",") { + ips := strings.Split(info.Permission, ",") + for _, ip := range ips { + if len(ip) != 0 { + userlist = append(userlist, fmt.Sprintf("'%s'@'%s'", info.Username, ip)) + } + } + } else { + userlist = append(userlist, fmt.Sprintf("'%s'@'%s'", info.Username, info.Permission)) + } + + for _, user := range userlist { + if strings.HasPrefix(info.Version, "5.6") { + if err := r.ExecSQL(fmt.Sprintf("drop user %s", user), info.Timeout); err != nil && !info.ForceDelete { + return err + } + } else { + if err := r.ExecSQL(fmt.Sprintf("drop user if exists %s", user), info.Timeout); err != nil && !info.ForceDelete { + return err + } + } + } + if len(info.Name) != 0 { + if err := r.ExecSQL(fmt.Sprintf("drop database if exists `%s`", info.Name), info.Timeout); err != nil && !info.ForceDelete { + return err + } + } + if !info.ForceDelete { + global.LOG.Info("execute delete database sql successful, now start to drop uploads and records") + } + + return nil +} + +func (r *Remote) ChangePassword(info PasswordChangeInfo) error { + if info.Username != "root" { + var userlist []string + if strings.Contains(info.Permission, ",") { + ips := strings.Split(info.Permission, ",") + for _, ip := range ips { + if len(ip) != 0 { + userlist = append(userlist, fmt.Sprintf("'%s'@'%s'", info.Username, ip)) + } + } + } else { + userlist = append(userlist, fmt.Sprintf("'%s'@'%s'", info.Username, info.Permission)) + } + + for _, user := range userlist { + passwordChangeSql := fmt.Sprintf("set password for %s = password('%s')", user, info.Password) + if !strings.HasPrefix(info.Version, "5.7") && !strings.HasPrefix(info.Version, "5.6") { + passwordChangeSql = fmt.Sprintf("ALTER USER %s IDENTIFIED BY '%s';", user, info.Password) + } + if err := r.ExecSQL(passwordChangeSql, info.Timeout); err != nil { + return err + } + } + return nil + } + + hosts, err := r.ExecSQLForHosts(info.Timeout) + if err != nil { + return err + } + for _, host := range hosts { + if host == "%" || host == "localhost" { + passwordRootChangeCMD := fmt.Sprintf("set password for 'root'@'%s' = password('%s')", host, info.Password) + if !strings.HasPrefix(info.Version, "5.7") && !strings.HasPrefix(info.Version, "5.6") { + passwordRootChangeCMD = fmt.Sprintf("alter user 'root'@'%s' identified by '%s';", host, info.Password) + } + if err := r.ExecSQL(passwordRootChangeCMD, info.Timeout); err != nil { + return err + } + } + } + + return nil +} + +func (r *Remote) ChangeAccess(info AccessChangeInfo) error { + if info.Username == "root" { + info.OldPermission = "%" + info.Name = "*" + info.Password = r.Password + } + if info.Permission != info.OldPermission { + if err := r.Delete(DeleteInfo{ + Version: info.Version, + Username: info.Username, + Permission: info.OldPermission, + ForceDelete: true, + Timeout: 300}); err != nil { + return err + } + if info.Username == "root" { + return nil + } + } + if err := r.CreateUser(CreateInfo{ + Name: info.Name, + Version: info.Version, + Username: info.Username, + Password: info.Password, + Permission: info.Permission, + Timeout: info.Timeout, + }, false); err != nil { + return err + } + if err := r.ExecSQL("flush privileges", 300); err != nil { + return err + } + return nil +} + +func (r *Remote) Backup(info BackupInfo) error { + fileOp := files.NewFileOp() + if !fileOp.Stat(info.TargetDir) { + if err := os.MkdirAll(info.TargetDir, os.ModePerm); err != nil { + return fmt.Errorf("mkdir %s failed, err: %v", info.TargetDir, err) + } + } + outfile, err := os.OpenFile(path.Join(info.TargetDir, info.FileName), os.O_RDWR|os.O_CREATE, 0755) + if err != nil { + return fmt.Errorf("open file %s failed, err: %v", path.Join(info.TargetDir, info.FileName), err) + } + defer outfile.Close() + dumpCmd := "mysqldump" + if r.Type == constant.AppMariaDB { + dumpCmd = "mariadb-dump" + } + global.LOG.Infof("start to %s | gzip > %s.gzip", dumpCmd, info.TargetDir+"/"+info.FileName) + image, err := loadImage(info.Type, info.Version) + if err != nil { + return err + } + backupCmd := fmt.Sprintf("docker run --rm --net=host -i %s /bin/bash -c '%s -h %s -P %d -u%s -p%s %s --default-character-set=%s %s'", + image, dumpCmd, r.Address, r.Port, r.User, r.Password, sslSkip(info.Version, r.Type), info.Format, info.Name) + + global.LOG.Debug(strings.ReplaceAll(backupCmd, r.Password, "******")) + cmd := exec.Command("bash", "-c", backupCmd) + var stderr bytes.Buffer + cmd.Stderr = &stderr + + gzipCmd := exec.Command("gzip", "-cf") + gzipCmd.Stdin, _ = cmd.StdoutPipe() + gzipCmd.Stdout = outfile + + _ = gzipCmd.Start() + if err := cmd.Run(); err != nil { + return fmt.Errorf("handle backup database failed, err: %v", stderr.String()) + } + _ = gzipCmd.Wait() + return nil +} + +func (r *Remote) Recover(info RecoverInfo) error { + fi, _ := os.Open(info.SourceFile) + defer fi.Close() + + image, err := loadImage(info.Type, info.Version) + if err != nil { + return err + } + + recoverCmd := fmt.Sprintf("docker run --rm --net=host -i %s /bin/bash -c '%s -h %s -P %d -u%s -p%s %s --default-character-set=%s %s'", + image, r.Type, r.Address, r.Port, r.User, r.Password, sslSkip(info.Version, r.Type), info.Format, info.Name) + + global.LOG.Debug(strings.ReplaceAll(recoverCmd, r.Password, "******")) + 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 +} + +func (r *Remote) SyncDB(version string) ([]SyncDBInfo, error) { + var datas []SyncDBInfo + rows, err := r.Client.Query("select schema_name, default_character_set_name from information_schema.SCHEMATA") + if err != nil { + return datas, err + } + defer rows.Close() + + for rows.Next() { + var dbName, charsetName string + if err = rows.Scan(&dbName, &charsetName); err != nil { + return datas, err + } + if dbName == "information_schema" || dbName == "mysql" || dbName == "performance_schema" || dbName == "sys" || dbName == "__recycle_bin__" || dbName == "recycle_bin" { + continue + } + dataItem := SyncDBInfo{ + Name: dbName, + From: "remote", + MysqlName: r.Database, + Format: charsetName, + } + userRows, err := r.Client.Query("select user,host from mysql.db where db = ?", dbName) + if err != nil { + global.LOG.Debugf("sync user of db %s failed, err: %v", dbName, err) + dataItem.Permission = "%" + datas = append(datas, dataItem) + continue + } + + var permissionItem []string + isLocal := true + i := 0 + for userRows.Next() { + var user, host string + if err = userRows.Scan(&user, &host); err != nil { + return datas, err + } + if user == "root" { + continue + } + if i == 0 { + dataItem.Username = user + } + if dataItem.Username == user && host == "%" { + isLocal = false + dataItem.Permission = "%" + } else if dataItem.Username == user && host != "localhost" { + isLocal = false + permissionItem = append(permissionItem, host) + } + i++ + } + if len(dataItem.Username) == 0 { + dataItem.Permission = "%" + } else { + if isLocal { + dataItem.Permission = "localhost" + } + if len(dataItem.Permission) == 0 { + dataItem.Permission = strings.Join(permissionItem, ",") + } + } + datas = append(datas, dataItem) + } + if err = rows.Err(); err != nil { + return datas, err + } + return datas, nil +} + +func (r *Remote) Close() { + _ = r.Client.Close() +} + +func (r *Remote) ExecSQL(command string, timeout uint) error { + ctx, cancel := context.WithTimeout(context.Background(), time.Duration(timeout)*time.Second) + defer cancel() + + if _, err := r.Client.ExecContext(ctx, command); err != nil { + return err + } + if errors.Is(ctx.Err(), context.DeadlineExceeded) { + return buserr.New(constant.ErrExecTimeOut) + } + + return nil +} + +func (r *Remote) ExecSQLForHosts(timeout uint) ([]string, error) { + ctx, cancel := context.WithTimeout(context.Background(), time.Duration(timeout)*time.Second) + defer cancel() + + results, err := r.Client.QueryContext(ctx, "select host from mysql.user where user='root';") + if err != nil { + return nil, err + } + if errors.Is(ctx.Err(), context.DeadlineExceeded) { + return nil, buserr.New(constant.ErrExecTimeOut) + } + var rows []string + for results.Next() { + var host string + if err := results.Scan(&host); err != nil { + continue + } + rows = append(rows, host) + } + return rows, nil +} + +func loadImage(dbType, version string) (string, error) { + cli, err := client.NewClientWithOpts(client.FromEnv, client.WithAPIVersionNegotiation()) + if err != nil { + return "", err + } + images, err := cli.ImageList(context.Background(), image.ListOptions{}) + if err != nil { + return "", 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 + } + } + } + return loadVersion(dbType, version), nil +} + +func loadVersion(dbType string, version string) string { + if dbType == "mariadb" { + return "mariadb:11.3.2 " + } + if strings.HasPrefix(version, "5.6") { + return "mysql:5.6.51" + } + if strings.HasPrefix(version, "5.7") { + return "mysql:5.7.44" + } + return "mysql:8.2.0" +} + +func sslSkip(version, dbType string) string { + if dbType == constant.AppMariaDB || strings.HasPrefix(version, "5.6") || strings.HasPrefix(version, "5.7") { + return "--skip-ssl" + } + return "--ssl-mode=DISABLED" +} diff --git a/agent/utils/mysql/helper/dump.go b/agent/utils/mysql/helper/dump.go new file mode 100644 index 000000000..3961db5e5 --- /dev/null +++ b/agent/utils/mysql/helper/dump.go @@ -0,0 +1,303 @@ +package helper + +import ( + "bufio" + "database/sql" + "fmt" + "io" + "os" + "strings" + "time" + + "github.com/1Panel-dev/1Panel/agent/constant" + "github.com/1Panel-dev/1Panel/agent/global" + _ "github.com/go-sql-driver/mysql" +) + +func init() {} + +type dumpOption struct { + isData bool + + tables []string + isAllTable bool + isDropTable bool + writer io.Writer +} + +type DumpOption func(*dumpOption) + +func WithDropTable() DumpOption { + return func(option *dumpOption) { + option.isDropTable = true + } +} + +func WithData() DumpOption { + return func(option *dumpOption) { + option.isData = true + } +} + +func WithWriter(writer io.Writer) DumpOption { + return func(option *dumpOption) { + option.writer = writer + } +} + +func Dump(dns string, opts ...DumpOption) error { + start := time.Now() + global.LOG.Infof("dump start at %s\n", start.Format(constant.DateTimeLayout)) + defer func() { + end := time.Now() + global.LOG.Infof("dump end at %s, cost %s\n", end.Format(constant.DateTimeLayout), end.Sub(start)) + }() + + var err error + + var o dumpOption + + for _, opt := range opts { + opt(&o) + } + + if len(o.tables) == 0 { + o.isAllTable = true + } + + if o.writer == nil { + o.writer = os.Stdout + } + + buf := bufio.NewWriter(o.writer) + defer buf.Flush() + + itemFile, lineNumber := "", 0 + + itemFile += "-- ----------------------------\n" + itemFile += "-- MySQL Database Dump\n" + itemFile += "-- Start Time: " + start.Format(constant.DateTimeLayout) + "\n" + itemFile += "-- ----------------------------\n\n\n" + + db, err := sql.Open("mysql", dns) + if err != nil { + global.LOG.Errorf("open mysql db failed, err: %v", err) + return err + } + defer db.Close() + + dbName, err := getDBNameFromDNS(dns) + if err != nil { + global.LOG.Errorf("get db name from dns failed, err: %v", err) + return err + } + _, err = db.Exec(fmt.Sprintf("USE `%s`", dbName)) + if err != nil { + global.LOG.Errorf("exec `use %s` failed, err: %v", dbName, err) + return err + } + + var tables []string + if o.isAllTable { + tmp, err := getAllTables(db) + if err != nil { + global.LOG.Errorf("get all tables failed, err: %v", err) + return err + } + tables = tmp + } else { + tables = o.tables + } + + for _, table := range tables { + if o.isDropTable { + itemFile += fmt.Sprintf("DROP TABLE IF EXISTS `%s`;\n", table) + } + + itemFile += "-- ----------------------------\n" + itemFile += fmt.Sprintf("-- Table structure for %s\n", table) + itemFile += "-- ----------------------------\n" + + createTableSQL, err := getCreateTableSQL(db, table) + if err != nil { + global.LOG.Errorf("get create table sql failed, err: %v", err) + return err + } + itemFile += createTableSQL + itemFile += ";\n\n\n\n" + + if o.isData { + itemFile += "-- ----------------------------\n" + itemFile += fmt.Sprintf("-- Records of %s\n", table) + itemFile += "-- ----------------------------\n" + + lineRows, err := db.Query(fmt.Sprintf("SELECT * FROM `%s`", table)) + if err != nil { + global.LOG.Errorf("exec `select * from %s` failed, err: %v", table, err) + return err + } + defer lineRows.Close() + + var columns []string + columns, err = lineRows.Columns() + if err != nil { + global.LOG.Errorf("get columes failed, err: %v", err) + return err + } + columnTypes, err := lineRows.ColumnTypes() + if err != nil { + global.LOG.Errorf("get colume types failed, err: %v", err) + return err + } + for lineRows.Next() { + row := make([]interface{}, len(columns)) + rowPointers := make([]interface{}, len(columns)) + for i := range columns { + rowPointers[i] = &row[i] + } + if err = lineRows.Scan(rowPointers...); err != nil { + global.LOG.Errorf("scan row data failed, err: %v", err) + return err + } + ssql := loadDataSql(row, columnTypes, table) + if len(ssql) != 0 { + itemFile += ssql + lineNumber++ + } + if lineNumber > 500 { + _, _ = buf.WriteString(itemFile) + itemFile = "" + lineNumber = 0 + } + } + + itemFile += "\n\n" + } + } + + itemFile += "-- ----------------------------\n" + itemFile += "-- Dumped by mysqldump\n" + itemFile += "-- Cost Time: " + time.Since(start).String() + "\n" + itemFile += "-- ----------------------------\n" + + _, _ = buf.WriteString(itemFile) + _ = buf.Flush() + + return nil +} + +func getCreateTableSQL(db *sql.DB, table string) (string, error) { + var createTableSQL string + err := db.QueryRow(fmt.Sprintf("SHOW CREATE TABLE `%s`", table)).Scan(&table, &createTableSQL) + if err != nil { + return "", err + } + createTableSQL = strings.Replace(createTableSQL, "CREATE TABLE", "CREATE TABLE IF NOT EXISTS", 1) + return createTableSQL, nil +} + +func getAllTables(db *sql.DB) ([]string, error) { + var tables []string + rows, err := db.Query("SHOW TABLES") + if err != nil { + return nil, err + } + defer rows.Close() + + for rows.Next() { + var table string + err = rows.Scan(&table) + if err != nil { + return nil, err + } + tables = append(tables, table) + } + return tables, nil +} + +func loadDataSql(row []interface{}, columnTypes []*sql.ColumnType, table string) string { + ssql := "INSERT INTO `" + table + "` VALUES (" + for i, col := range row { + if col == nil { + ssql += "NULL" + } else { + Type := columnTypes[i].DatabaseTypeName() + Type = strings.Replace(Type, "UNSIGNED", "", -1) + Type = strings.Replace(Type, " ", "", -1) + switch Type { + case "TINYINT", "SMALLINT", "MEDIUMINT", "INT", "INTEGER", "BIGINT": + if bs, ok := col.([]byte); ok { + ssql += string(bs) + } else { + ssql += fmt.Sprintf("%d", col) + } + case "FLOAT", "DOUBLE": + if bs, ok := col.([]byte); ok { + ssql += string(bs) + } else { + ssql += fmt.Sprintf("%f", col) + } + case "DECIMAL", "DEC": + ssql += fmt.Sprintf("%s", col) + + case "DATE": + t, ok := col.(time.Time) + if !ok { + global.LOG.Errorf("the DATE type conversion failed, err value: %v", col) + return "" + } + ssql += fmt.Sprintf("'%s'", t.Format("2006-01-02")) + case "DATETIME": + t, ok := col.(time.Time) + if !ok { + global.LOG.Errorf("the DATETIME type conversion failed, err value: %v", col) + return "" + } + ssql += fmt.Sprintf("'%s'", t.Format(constant.DateTimeLayout)) + case "TIMESTAMP": + t, ok := col.(time.Time) + if !ok { + global.LOG.Errorf("the TIMESTAMP type conversion failed, err value: %v", col) + return "" + } + ssql += fmt.Sprintf("'%s'", t.Format(constant.DateTimeLayout)) + case "TIME": + t, ok := col.([]byte) + if !ok { + global.LOG.Errorf("the TIME type conversion failed, err value: %v", col) + return "" + } + ssql += fmt.Sprintf("'%s'", string(t)) + case "YEAR": + t, ok := col.([]byte) + if !ok { + global.LOG.Errorf("the YEAR type conversion failed, err value: %v", col) + return "" + } + ssql += string(t) + case "CHAR", "VARCHAR", "TINYTEXT", "TEXT", "MEDIUMTEXT", "LONGTEXT": + ssql += fmt.Sprintf("'%s'", strings.Replace(fmt.Sprintf("%s", col), "'", "''", -1)) + case "BIT", "BINARY", "VARBINARY", "TINYBLOB", "BLOB", "MEDIUMBLOB", "LONGBLOB": + ssql += fmt.Sprintf("0x%X", col) + case "ENUM", "SET": + ssql += fmt.Sprintf("'%s'", col) + case "BOOL", "BOOLEAN": + if col.(bool) { + ssql += "true" + } else { + ssql += "false" + } + case "JSON": + ssql += fmt.Sprintf("'%s'", col) + default: + global.LOG.Errorf("unsupported colume type: %s", Type) + return "" + } + } + if i < len(row)-1 { + ssql += "," + } + } + ssql += ");\n" + return ssql +} diff --git a/agent/utils/mysql/helper/source.go b/agent/utils/mysql/helper/source.go new file mode 100644 index 000000000..f4b37ed98 --- /dev/null +++ b/agent/utils/mysql/helper/source.go @@ -0,0 +1,244 @@ +package helper + +import ( + "bufio" + "database/sql" + "errors" + "fmt" + "io" + "strings" + "time" + + "github.com/1Panel-dev/1Panel/agent/constant" + "github.com/1Panel-dev/1Panel/agent/global" +) + +type sourceOption struct { + dryRun bool + mergeInsert int + debug bool +} +type SourceOption func(*sourceOption) + +func WithMergeInsert(size int) SourceOption { + return func(o *sourceOption) { + o.mergeInsert = size + } +} + +type dbWrapper struct { + DB *sql.DB + debug bool + dryRun bool +} + +func newDBWrapper(db *sql.DB, dryRun, debug bool) *dbWrapper { + + return &dbWrapper{ + DB: db, + dryRun: dryRun, + debug: debug, + } +} + +func (db *dbWrapper) Exec(query string, args ...interface{}) (sql.Result, error) { + if db.debug { + global.LOG.Debugf("query %s", query) + } + + if db.dryRun { + return nil, nil + } + return db.DB.Exec(query, args...) +} + +func Source(dns string, reader io.Reader, opts ...SourceOption) error { + start := time.Now() + global.LOG.Infof("source start at %s", start.Format(constant.DateTimeLayout)) + defer func() { + end := time.Now() + global.LOG.Infof("source end at %s, cost %s", end.Format(constant.DateTimeLayout), end.Sub(start)) + }() + + var err error + var db *sql.DB + var o sourceOption + for _, opt := range opts { + opt(&o) + } + + dbName, err := getDBNameFromDNS(dns) + if err != nil { + global.LOG.Errorf("get db name from dns failed, err: %v", err) + return err + } + + db, err = sql.Open("mysql", dns) + if err != nil { + global.LOG.Errorf("open mysql db failed, err: %v", err) + return err + } + defer db.Close() + + dbWrapper := newDBWrapper(db, o.dryRun, o.debug) + + _, err = dbWrapper.Exec(fmt.Sprintf("USE `%s`;", dbName)) + if err != nil { + global.LOG.Errorf("exec `use %s` failed, err: %v", dbName, err) + return err + } + + db.SetConnMaxLifetime(3600) + + r := bufio.NewReader(reader) + _, err = dbWrapper.Exec("SET autocommit=0;") + if err != nil { + global.LOG.Errorf("exec `set autocommit=0` failed, err: %v", err) + return err + } + + for { + line, err := readLine(r) + if err != nil { + if err == io.EOF { + break + } + global.LOG.Errorf("read sql failed, err: %v", err) + return err + } + + ssql, err := trim(line) + if err != nil { + global.LOG.Errorf("trim sql failed, err: %v", err) + return err + } + + afterInsertSql := "" + if o.mergeInsert > 1 && strings.HasPrefix(ssql, "INSERT INTO") { + var insertSQLs []string + insertSQLs = append(insertSQLs, ssql) + for i := 0; i < o.mergeInsert-1; i++ { + line, err := readLine(r) + if err != nil { + if err == io.EOF { + break + } + return err + } + ssql2, err := trim(line) + if err != nil { + global.LOG.Errorf("trim merge insert sql failed, err: %v", err) + return err + } + if strings.HasPrefix(ssql2, "INSERT INTO") { + insertSQLs = append(insertSQLs, ssql2) + continue + } + afterInsertSql = ssql2 + break + } + ssql, err = mergeInsert(insertSQLs) + if err != nil { + global.LOG.Errorf("do merge insert failed, err: %v", err) + return err + } + } + + _, err = dbWrapper.Exec(ssql) + if err != nil { + global.LOG.Errorf("exec sql failed, err: %v", err) + return err + } + if len(afterInsertSql) != 0 { + _, err = dbWrapper.Exec(afterInsertSql) + if err != nil { + global.LOG.Errorf("exec sql failed, err: %v", err) + return err + } + } + } + + _, err = dbWrapper.Exec("COMMIT;") + if err != nil { + global.LOG.Errorf("exec `commit` failed, err: %v", err) + return err + } + + _, err = dbWrapper.Exec("SET autocommit=1;") + if err != nil { + global.LOG.Errorf("exec `autocommit=1` failed, err: %v", err) + return err + } + + return nil +} + +func mergeInsert(insertSQLs []string) (string, error) { + if len(insertSQLs) == 0 { + return "", errors.New("no input provided") + } + builder := strings.Builder{} + sql1 := insertSQLs[0] + sql1 = strings.TrimSuffix(sql1, ";") + builder.WriteString(sql1) + for i, insertSQL := range insertSQLs[1:] { + if i < len(insertSQLs)-1 { + builder.WriteString(",") + } + + valuesIdx := strings.Index(insertSQL, "VALUES") + if valuesIdx == -1 { + return "", errors.New("invalid SQL: missing VALUES keyword") + } + sqln := insertSQL[valuesIdx:] + sqln = strings.TrimPrefix(sqln, "VALUES") + sqln = strings.TrimSuffix(sqln, ";") + builder.WriteString(sqln) + + } + builder.WriteString(";") + + return builder.String(), nil +} + +func trim(s string) (string, error) { + s = strings.TrimLeft(s, "\n") + s = strings.TrimSpace(s) + return s, nil +} + +func getDBNameFromDNS(dns string) (string, error) { + ss1 := strings.Split(dns, "/") + if len(ss1) == 2 { + ss2 := strings.Split(ss1[1], "?") + if len(ss2) == 2 { + return ss2[0], nil + } + } + + return "", fmt.Errorf("dns error: %s", dns) +} + +func readLine(r *bufio.Reader) (string, error) { + lineItem, err := r.ReadString('\n') + if err != nil { + if err == io.EOF { + return lineItem, err + } + global.LOG.Errorf("read merge insert sql failed, err: %v", err) + return "", err + } + if strings.HasSuffix(lineItem, ";\n") { + return lineItem, nil + } + lineAppend, err := readLine(r) + if err != nil { + if err == io.EOF { + return lineItem, err + } + global.LOG.Errorf("read merge insert sql failed, err: %v", err) + return "", err + } + + return lineItem + lineAppend, nil +} diff --git a/agent/utils/nginx/components/block.go b/agent/utils/nginx/components/block.go new file mode 100644 index 000000000..0e07e7793 --- /dev/null +++ b/agent/utils/nginx/components/block.go @@ -0,0 +1,88 @@ +package components + +type Block struct { + Line int + Comment string + Directives []IDirective + IsLuaBlock bool + LiteralCode string +} + +func (b *Block) GetDirectives() []IDirective { + return b.Directives +} + +func (b *Block) GetComment() string { + return b.Comment +} + +func (b *Block) GetLine() int { + return b.Line +} + +func (b *Block) GetCodeBlock() string { + return b.LiteralCode +} + +func (b *Block) FindDirectives(directiveName string) []IDirective { + directives := make([]IDirective, 0) + for _, directive := range b.GetDirectives() { + if directive.GetName() == directiveName { + directives = append(directives, directive) + } + if directive.GetBlock() != nil { + directives = append(directives, directive.GetBlock().FindDirectives(directiveName)...) + } + } + + return directives +} + +func (b *Block) UpdateDirective(key string, params []string) { + if key == "" || len(params) == 0 { + return + } + directives := b.GetDirectives() + index := -1 + for i, dir := range directives { + if dir.GetName() == key { + if IsRepeatKey(key) { + oldParams := dir.GetParameters() + if !(len(oldParams) > 0 && oldParams[0] == params[0]) { + continue + } + } + index = i + break + } + } + newDirective := &Directive{ + Name: key, + Parameters: params, + } + if index > -1 { + directives[index] = newDirective + } else { + directives = append(directives, newDirective) + } + b.Directives = directives +} + +func (b *Block) RemoveDirective(key string, params []string) { + directives := b.GetDirectives() + var newDirectives []IDirective + for _, dir := range directives { + if dir.GetName() == key { + if IsRepeatKey(key) && len(params) > 0 { + oldParams := dir.GetParameters() + if oldParams[0] == params[0] { + continue + } + } else { + continue + } + } + newDirectives = append(newDirectives, dir) + } + b.Directives = newDirectives +} diff --git a/agent/utils/nginx/components/comment.go b/agent/utils/nginx/components/comment.go new file mode 100644 index 000000000..31459be09 --- /dev/null +++ b/agent/utils/nginx/components/comment.go @@ -0,0 +1,26 @@ +package components + +type Comment struct { + Detail string + Line int +} + +func (c *Comment) GetName() string { + return "" +} + +func (c *Comment) GetParameters() []string { + return []string{} +} + +func (c *Comment) GetBlock() IBlock { + return nil +} + +func (c *Comment) GetComment() string { + return c.Detail +} + +func (c *Comment) GetLine() int { + return c.Line +} diff --git a/agent/utils/nginx/components/config.go b/agent/utils/nginx/components/config.go new file mode 100644 index 000000000..7f9b5bce9 --- /dev/null +++ b/agent/utils/nginx/components/config.go @@ -0,0 +1,45 @@ +package components + +type Config struct { + *Block + FilePath string +} + +func (c *Config) FindServers() []*Server { + var servers []*Server + directives := c.Block.FindDirectives("server") + for _, directive := range directives { + servers = append(servers, directive.(*Server)) + } + return servers +} + +func (c *Config) FindHttp() *Http { + var http *Http + directives := c.Block.FindDirectives("http") + if len(directives) > 0 { + http = directives[0].(*Http) + } + + return http +} + +var repeatKeys = map[string]struct { +}{ + "limit_conn": {}, + "limit_conn_zone": {}, + "set": {}, + "if": {}, + "proxy_set_header": {}, + "location": {}, + "include": {}, + "sub_filter": {}, + "add_header": {}, +} + +func IsRepeatKey(key string) bool { + if _, ok := repeatKeys[key]; ok { + return true + } + return false +} diff --git a/agent/utils/nginx/components/directive.go b/agent/utils/nginx/components/directive.go new file mode 100644 index 000000000..dfdef75ef --- /dev/null +++ b/agent/utils/nginx/components/directive.go @@ -0,0 +1,29 @@ +package components + +type Directive struct { + Line int + Block IBlock + Name string + Comment string + Parameters []string +} + +func (d *Directive) GetComment() string { + return d.Comment +} + +func (d *Directive) GetName() string { + return d.Name +} + +func (d *Directive) GetParameters() []string { + return d.Parameters +} + +func (d *Directive) GetBlock() IBlock { + return d.Block +} + +func (d *Directive) GetLine() int { + return d.Line +} diff --git a/agent/utils/nginx/components/http.go b/agent/utils/nginx/components/http.go new file mode 100644 index 000000000..ff9d3028c --- /dev/null +++ b/agent/utils/nginx/components/http.go @@ -0,0 +1,129 @@ +package components + +import ( + "errors" +) + +type Http struct { + Comment string + Servers []*Server + Directives []IDirective + Line int +} + +func (h *Http) GetCodeBlock() string { + return "" +} + +func (h *Http) GetComment() string { + return h.Comment +} + +func NewHttp(directive IDirective) (*Http, error) { + if block := directive.GetBlock(); block != nil { + http := &Http{ + Line: directive.GetBlock().GetLine(), + Servers: []*Server{}, + Directives: []IDirective{}, + Comment: block.GetComment(), + } + for _, directive := range block.GetDirectives() { + if server, ok := directive.(*Server); ok { + http.Servers = append(http.Servers, server) + continue + } + http.Directives = append(http.Directives, directive) + } + + return http, nil + } + return nil, errors.New("http directive must have a block") +} + +func (h *Http) GetName() string { + return "http" +} + +func (h *Http) GetParameters() []string { + return []string{} +} + +func (h *Http) GetDirectives() []IDirective { + directives := make([]IDirective, 0) + directives = append(directives, h.Directives...) + for _, directive := range h.Servers { + directives = append(directives, directive) + } + return directives +} + +func (h *Http) FindDirectives(directiveName string) []IDirective { + directives := make([]IDirective, 0) + for _, directive := range h.GetDirectives() { + if directive.GetName() == directiveName { + directives = append(directives, directive) + } + if directive.GetBlock() != nil { + directives = append(directives, directive.GetBlock().FindDirectives(directiveName)...) + } + } + + return directives +} + +func (h *Http) UpdateDirective(key string, params []string) { + if key == "" || len(params) == 0 { + return + } + directives := h.GetDirectives() + index := -1 + for i, dir := range directives { + if dir.GetName() == key { + if IsRepeatKey(key) { + oldParams := dir.GetParameters() + if !(len(oldParams) > 0 && oldParams[0] == params[0]) { + continue + } + } + index = i + break + } + } + newDirective := &Directive{ + Name: key, + Parameters: params, + } + if index > -1 { + directives[index] = newDirective + } else { + directives = append(directives, newDirective) + } + h.Directives = directives +} + +func (h *Http) RemoveDirective(key string, params []string) { + directives := h.GetDirectives() + var newDirectives []IDirective + for _, dir := range directives { + if dir.GetName() == key { + if IsRepeatKey(key) && len(params) > 0 { + oldParams := dir.GetParameters() + if oldParams[0] == params[0] { + continue + } + } else { + continue + } + } + newDirectives = append(newDirectives, dir) + } + h.Directives = newDirectives +} + +func (h *Http) GetBlock() IBlock { + return h +} + +func (h *Http) GetLine() int { + return h.Line +} diff --git a/agent/utils/nginx/components/location.go b/agent/utils/nginx/components/location.go new file mode 100644 index 000000000..c407c0ede --- /dev/null +++ b/agent/utils/nginx/components/location.go @@ -0,0 +1,244 @@ +package components + +import ( + "fmt" + "regexp" + "strconv" + "strings" +) + +type Location struct { + Modifier string + Match string + Cache bool + ProxyPass string + Host string + CacheTime int + CacheUint string + Comment string + Directives []IDirective + Line int + Parameters []string + Replaces map[string]string +} + +func (l *Location) GetCodeBlock() string { + return "" +} + +func NewLocation(directive IDirective) *Location { + location := &Location{ + Modifier: "", + Match: "", + } + directives := make([]IDirective, 0) + if len(directive.GetParameters()) == 0 { + panic("no enough parameter for location") + } + for _, dir := range directive.GetBlock().GetDirectives() { + directives = append(directives, dir) + params := dir.GetParameters() + switch dir.GetName() { + case "proxy_pass": + location.ProxyPass = params[0] + case "proxy_set_header": + if params[0] == "Host" { + location.Host = params[1] + } + case "proxy_cache": + location.Cache = true + case "if": + if params[0] == "(" && params[1] == "$uri" && params[2] == "~*" { + dirs := dir.GetBlock().GetDirectives() + for _, di := range dirs { + if di.GetName() == "expires" { + re := regexp.MustCompile(`^(\d+)(\w+)$`) + matches := re.FindStringSubmatch(di.GetParameters()[0]) + if matches == nil { + continue + } + cacheTime, err := strconv.Atoi(matches[1]) + if err != nil { + continue + } + unit := matches[2] + location.CacheUint = unit + location.CacheTime = cacheTime + } + } + } + case "sub_filter": + if location.Replaces == nil { + location.Replaces = make(map[string]string, 0) + } + location.Replaces[strings.Trim(params[0], "\"")] = strings.Trim(params[1], "\"") + } + } + + params := directive.GetParameters() + if len(params) == 1 { + location.Match = params[0] + } else if len(params) == 2 { + location.Match = params[1] + location.Modifier = params[0] + } + location.Parameters = directive.GetParameters() + location.Line = directive.GetLine() + location.Comment = directive.GetComment() + location.Directives = directives + return location +} + +func (l *Location) GetName() string { + return "location" +} + +func (l *Location) GetParameters() []string { + return l.Parameters +} + +func (l *Location) GetBlock() IBlock { + return l +} + +func (l *Location) GetComment() string { + return l.Comment +} + +func (l *Location) GetLine() int { + return l.Line +} + +func (l *Location) GetDirectives() []IDirective { + return l.Directives +} + +func (l *Location) FindDirectives(directiveName string) []IDirective { + directives := make([]IDirective, 0) + for _, directive := range l.Directives { + if directive.GetName() == directiveName { + directives = append(directives, directive) + } + if directive.GetBlock() != nil { + directives = append(directives, directive.GetBlock().FindDirectives(directiveName)...) + } + } + return directives +} + +func (l *Location) UpdateDirective(key string, params []string) { + if key == "" || len(params) == 0 { + return + } + directives := l.Directives + index := -1 + for i, dir := range directives { + if dir.GetName() == key { + if IsRepeatKey(key) { + oldParams := dir.GetParameters() + if !(len(oldParams) > 0 && oldParams[0] == params[0]) { + continue + } + } + index = i + break + } + } + newDirective := &Directive{ + Name: key, + Parameters: params, + } + if index > -1 { + directives[index] = newDirective + } else { + directives = append(directives, newDirective) + } + l.Directives = directives +} + +func (l *Location) RemoveDirective(key string, params []string) { + directives := l.Directives + var newDirectives []IDirective + for _, dir := range directives { + if dir.GetName() == key { + if len(params) > 0 { + oldParams := dir.GetParameters() + if oldParams[0] == params[0] { + continue + } + } else { + continue + } + } + newDirectives = append(newDirectives, dir) + } + l.Directives = newDirectives +} + +func (l *Location) ChangePath(Modifier string, Match string) { + if Match != "" && Modifier != "" { + l.Parameters = []string{Modifier, Match} + } + if Match != "" && Modifier == "" { + l.Parameters = []string{Match} + } + l.Modifier = Modifier + l.Match = Match +} + +func (l *Location) AddCache(cacheTime int, cacheUint string) { + l.RemoveDirective("add_header", []string{"Cache-Control", "no-cache"}) + l.RemoveDirective("if", []string{"(", "$uri", "~*", `"\.(gif|png|jpg|css|js|woff|woff2)$"`, ")"}) + directives := l.GetDirectives() + newDir := &Directive{ + Name: "if", + Parameters: []string{"(", "$uri", "~*", `"\.(gif|png|jpg|css|js|woff|woff2)$"`, ")"}, + Block: &Block{}, + } + block := &Block{} + block.Directives = append(block.Directives, &Directive{ + Name: "expires", + Parameters: []string{strconv.Itoa(cacheTime) + cacheUint}, + }) + newDir.Block = block + directives = append(directives, newDir) + l.Directives = directives + l.UpdateDirective("proxy_ignore_headers", []string{"Set-Cookie", "Cache-Control", "expires"}) + l.UpdateDirective("proxy_cache", []string{"proxy_cache_panel"}) + l.UpdateDirective("proxy_cache_key", []string{"$host$uri$is_args$args"}) + l.UpdateDirective("proxy_cache_valid", []string{"200", "304", "301", "302", "10m"}) + l.Cache = true + l.CacheTime = cacheTime + l.CacheUint = cacheUint +} + +func (l *Location) RemoveCache() { + l.RemoveDirective("if", []string{"(", "$uri", "~*", `"\.(gif|png|jpg|css|js|woff|woff2)$"`, ")"}) + l.RemoveDirective("proxy_ignore_headers", []string{"Set-Cookie"}) + l.RemoveDirective("proxy_cache", []string{"proxy_cache_panel"}) + l.RemoveDirective("proxy_cache_key", []string{"$host$uri$is_args$args"}) + l.RemoveDirective("proxy_cache_valid", []string{"200"}) + + l.UpdateDirective("add_header", []string{"Cache-Control", "no-cache"}) + + l.CacheTime = 0 + l.CacheUint = "" + l.Cache = false +} + +func (l *Location) AddSubFilter(subFilters map[string]string) { + l.RemoveDirective("sub_filter", []string{}) + l.Replaces = subFilters + for k, v := range subFilters { + l.UpdateDirective("sub_filter", []string{fmt.Sprintf(`"%s"`, k), fmt.Sprintf(`"%s"`, v)}) + } + l.UpdateDirective("proxy_set_header", []string{"Accept-Encoding", `""`}) + l.UpdateDirective("sub_filter_once", []string{"off"}) +} + +func (l *Location) RemoveSubFilter() { + l.RemoveDirective("sub_filter", []string{}) + l.RemoveDirective("proxy_set_header", []string{"Accept-Encoding", `""`}) + l.RemoveDirective("sub_filter_once", []string{"off"}) + l.Replaces = nil +} diff --git a/agent/utils/nginx/components/lua_block.go b/agent/utils/nginx/components/lua_block.go new file mode 100644 index 000000000..d9b4b3676 --- /dev/null +++ b/agent/utils/nginx/components/lua_block.go @@ -0,0 +1,120 @@ +package components + +import ( + "fmt" +) + +type LuaBlock struct { + Directives []IDirective + Name string + Comment string + LuaCode string + Line int +} + +func NewLuaBlock(directive IDirective) (*LuaBlock, error) { + if block := directive.GetBlock(); block != nil { + lb := &LuaBlock{ + Directives: []IDirective{}, + Name: directive.GetName(), + LuaCode: block.GetCodeBlock(), + } + + lb.Directives = append(lb.Directives, block.GetDirectives()...) + return lb, nil + } + return nil, fmt.Errorf("%s must have a block", directive.GetName()) +} + +func (lb *LuaBlock) GetName() string { + return lb.Name +} + +func (lb *LuaBlock) GetParameters() []string { + return []string{} +} + +func (lb *LuaBlock) GetDirectives() []IDirective { + directives := make([]IDirective, 0) + directives = append(directives, lb.Directives...) + return directives +} + +func (lb *LuaBlock) FindDirectives(directiveName string) []IDirective { + directives := make([]IDirective, 0) + for _, directive := range lb.GetDirectives() { + if directive.GetName() == directiveName { + directives = append(directives, directive) + } + if directive.GetBlock() != nil { + directives = append(directives, directive.GetBlock().FindDirectives(directiveName)...) + } + } + + return directives +} + +func (lb *LuaBlock) GetCodeBlock() string { + return lb.LuaCode +} + +func (lb *LuaBlock) GetBlock() IBlock { + return lb +} + +func (lb *LuaBlock) GetComment() string { + return lb.Comment +} + +func (lb *LuaBlock) RemoveDirective(key string, params []string) { + directives := lb.Directives + var newDirectives []IDirective + for _, dir := range directives { + if dir.GetName() == key { + if len(params) > 0 { + oldParams := dir.GetParameters() + if oldParams[0] == params[0] { + continue + } + } else { + continue + } + } + newDirectives = append(newDirectives, dir) + } + lb.Directives = newDirectives +} + +func (lb *LuaBlock) UpdateDirective(key string, params []string) { + if key == "" || len(params) == 0 { + return + } + directives := lb.Directives + index := -1 + for i, dir := range directives { + if dir.GetName() == key { + if IsRepeatKey(key) { + oldParams := dir.GetParameters() + if !(len(oldParams) > 0 && oldParams[0] == params[0]) { + continue + } + } + index = i + break + } + } + newDirective := &Directive{ + Name: key, + Parameters: params, + } + if index > -1 { + directives[index] = newDirective + } else { + directives = append(directives, newDirective) + } + lb.Directives = directives +} + +func (lb *LuaBlock) GetLine() int { + return lb.Line +} diff --git a/agent/utils/nginx/components/server.go b/agent/utils/nginx/components/server.go new file mode 100644 index 000000000..c7138936f --- /dev/null +++ b/agent/utils/nginx/components/server.go @@ -0,0 +1,371 @@ +package components + +import ( + "errors" +) + +type Server struct { + Comment string + Listens []*ServerListen + Directives []IDirective + Line int +} + +func (s *Server) GetCodeBlock() string { + return "" +} + +func NewServer(directive IDirective) (*Server, error) { + server := &Server{} + if block := directive.GetBlock(); block != nil { + server.Line = directive.GetBlock().GetLine() + server.Comment = block.GetComment() + directives := block.GetDirectives() + for _, dir := range directives { + switch dir.GetName() { + case "listen": + server.Listens = append(server.Listens, NewServerListen(dir.GetParameters(), dir.GetLine())) + default: + server.Directives = append(server.Directives, dir) + } + } + return server, nil + } + return nil, errors.New("server directive must have a block") +} + +func (s *Server) GetName() string { + return "server" +} + +func (s *Server) GetParameters() []string { + return []string{} +} + +func (s *Server) GetBlock() IBlock { + return s +} + +func (s *Server) GetComment() string { + return s.Comment +} + +func (s *Server) GetDirectives() []IDirective { + directives := make([]IDirective, 0) + for _, ls := range s.Listens { + directives = append(directives, ls) + } + directives = append(directives, s.Directives...) + return directives +} + +func (s *Server) FindDirectives(directiveName string) []IDirective { + directives := make([]IDirective, 0) + for _, directive := range s.Directives { + if directive.GetName() == directiveName { + directives = append(directives, directive) + } + if directive.GetBlock() != nil { + directives = append(directives, directive.GetBlock().FindDirectives(directiveName)...) + } + } + if directiveName == "listen" { + for _, listen := range s.Listens { + params := []string{listen.Bind} + params = append(params, listen.Parameters...) + if listen.DefaultServer != "" { + params = append(params, DefaultServer) + } + directives = append(directives, &Directive{ + Name: "listen", + Parameters: params, + }) + } + } + return directives +} + +func (s *Server) UpdateDirective(key string, params []string) { + if key == "" || len(params) == 0 { + return + } + if key == "listen" { + defaultServer := false + paramLen := len(params) + if paramLen > 0 && params[paramLen-1] == "default_server" { + params = params[:paramLen-1] + defaultServer = true + } + s.UpdateListen(params[0], defaultServer, params[1:]...) + return + } + + directives := s.Directives + index := -1 + for i, dir := range directives { + if dir.GetName() == key { + if IsRepeatKey(key) { + oldParams := dir.GetParameters() + if !(len(oldParams) > 0 && oldParams[0] == params[0]) { + continue + } + } + index = i + break + } + } + newDirective := &Directive{ + Name: key, + Parameters: params, + } + if index > -1 { + directives[index] = newDirective + } else { + directives = append(directives, newDirective) + } + s.Directives = directives +} + +func (s *Server) RemoveDirective(key string, params []string) { + directives := s.Directives + var newDirectives []IDirective + for _, dir := range directives { + if dir.GetName() == key { + if len(params) == 0 { + continue + } + oldParams := dir.GetParameters() + if key == "location" { + if len(params) == len(oldParams) { + exist := true + for i := range params { + if params[i] != oldParams[i] { + exist = false + break + } + } + if exist { + continue + } + } + } else { + if oldParams[0] == params[0] { + continue + } + } + } + newDirectives = append(newDirectives, dir) + } + s.Directives = newDirectives +} + +func (s *Server) GetLine() int { + return s.Line +} + +func (s *Server) AddListen(bind string, defaultServer bool, params ...string) { + listen := &ServerListen{ + Bind: bind, + Parameters: params, + } + if defaultServer { + listen.DefaultServer = DefaultServer + } + s.Listens = append(s.Listens, listen) +} + +func (s *Server) UpdateListen(bind string, defaultServer bool, params ...string) { + listen := &ServerListen{ + Bind: bind, + Parameters: params, + } + if defaultServer { + listen.DefaultServer = DefaultServer + } + var newListens []*ServerListen + exist := false + for _, li := range s.Listens { + if li.Bind == bind { + exist = true + newListens = append(newListens, listen) + } else { + newListens = append(newListens, li) + } + } + if !exist { + newListens = append(newListens, listen) + } + + s.Listens = newListens +} + +func (s *Server) DeleteListen(bind string) { + var newListens []*ServerListen + for _, li := range s.Listens { + if li.Bind != bind { + newListens = append(newListens, li) + } + } + s.Listens = newListens +} + +func (s *Server) DeleteServerName(name string) { + var names []string + dirs := s.FindDirectives("server_name") + params := dirs[0].GetParameters() + for _, param := range params { + if param != name { + names = append(names, param) + } + } + s.UpdateServerName(names) +} + +func (s *Server) AddServerName(name string) { + dirs := s.FindDirectives("server_name") + params := dirs[0].GetParameters() + params = append(params, name) + s.UpdateServerName(params) +} + +func (s *Server) UpdateServerName(names []string) { + s.UpdateDirective("server_name", names) +} + +func (s *Server) UpdateRoot(path string) { + s.UpdateDirective("root", []string{path}) +} + +func (s *Server) UpdateRootLocation() { + newDir := Directive{ + Name: "location", + Parameters: []string{"/"}, + Block: &Block{}, + } + block := &Block{} + block.Directives = append(block.Directives, &Directive{ + Name: "root", + Parameters: []string{"index.html"}, + }) + newDir.Block = block +} + +func (s *Server) UpdateRootProxy(proxy []string) { + newDir := Directive{ + Name: "location", + Parameters: []string{"/"}, + Block: &Block{}, + } + block := &Block{} + block.Directives = append(block.Directives, &Directive{ + Name: "proxy_pass", + Parameters: proxy, + }) + newDir.Block = block + s.UpdateDirectiveBySecondKey("location", "/", newDir) +} + +func (s *Server) UpdatePHPProxy(proxy []string, localPath string) { + newDir := Directive{ + Name: "location", + Parameters: []string{"~ [^/]\\.php(/|$)"}, + Block: &Block{}, + } + block := &Block{} + block.Directives = append(block.Directives, &Directive{ + Name: "fastcgi_pass", + Parameters: proxy, + }) + block.Directives = append(block.Directives, &Directive{ + Name: "include", + Parameters: []string{"fastcgi-php.conf"}, + }) + block.Directives = append(block.Directives, &Directive{ + Name: "include", + Parameters: []string{"fastcgi_params"}, + }) + if localPath == "" { + block.Directives = append(block.Directives, &Directive{ + Name: "set", + Parameters: []string{"$real_script_name", "$fastcgi_script_name"}, + }) + ifDir := &Directive{ + Name: "if", + Parameters: []string{"($fastcgi_script_name ~ \"^(.+?\\.php)(/.+)$\")"}, + } + ifDir.Block = &Block{ + Directives: []IDirective{ + &Directive{ + Name: "set", + Parameters: []string{"$real_script_name", "$1"}, + }, + &Directive{ + Name: "set", + Parameters: []string{"$path_info", "$2"}, + }, + }, + } + block.Directives = append(block.Directives, ifDir) + block.Directives = append(block.Directives, &Directive{ + Name: "fastcgi_param", + Parameters: []string{"SCRIPT_FILENAME", "$document_root$real_script_name"}, + }) + block.Directives = append(block.Directives, &Directive{ + Name: "fastcgi_param", + Parameters: []string{"SCRIPT_NAME", "$real_script_name"}, + }) + block.Directives = append(block.Directives, &Directive{ + Name: "fastcgi_param", + Parameters: []string{"PATH_INFO", "$path_info"}, + }) + } else { + block.Directives = append(block.Directives, &Directive{ + Name: "fastcgi_param", + Parameters: []string{"SCRIPT_FILENAME", localPath}, + }) + } + newDir.Block = block + s.UpdateDirectiveBySecondKey("location", "~ [^/]\\.php(/|$)", newDir) +} + +func (s *Server) UpdateDirectiveBySecondKey(name string, key string, directive Directive) { + directives := s.Directives + index := -1 + for i, dir := range directives { + if dir.GetName() == name && dir.GetParameters()[0] == key { + index = i + break + } + } + if index > -1 { + directives[index] = &directive + } else { + directives = append(directives, &directive) + } + s.Directives = directives +} + +func (s *Server) RemoveListenByBind(bind string) { + var listens []*ServerListen + for _, listen := range s.Listens { + if listen.Bind != bind { + listens = append(listens, listen) + } + } + s.Listens = listens +} + +func (s *Server) AddHTTP2HTTPS() { + newDir := Directive{ + Name: "if", + Parameters: []string{"($scheme = http)"}, + Block: &Block{}, + } + block := &Block{} + block.Directives = append(block.Directives, &Directive{ + Name: "return", + Parameters: []string{"301", "https://$host$request_uri"}, + }) + newDir.Block = block + s.UpdateDirectiveBySecondKey("if", "($scheme", newDir) +} diff --git a/agent/utils/nginx/components/server_listen.go b/agent/utils/nginx/components/server_listen.go new file mode 100644 index 000000000..d45bb7e4d --- /dev/null +++ b/agent/utils/nginx/components/server_listen.go @@ -0,0 +1,75 @@ +package components + +import ( + "strings" + + "github.com/1Panel-dev/1Panel/agent/utils/common" +) + +const DefaultServer = "default_server" + +type ServerListen struct { + Bind string + DefaultServer string + Parameters []string + Comment string + Line int +} + +func NewServerListen(params []string, line int) *ServerListen { + server := &ServerListen{ + Parameters: []string{}, + Line: line, + } + for _, param := range params { + if isBind(param) { + server.Bind = param + } else if param == DefaultServer { + server.DefaultServer = DefaultServer + } else { + server.Parameters = append(server.Parameters, param) + } + } + return server +} + +func isBind(param string) bool { + if common.IsNum(param) { + return true + } + if strings.Contains(param, "*") || strings.Contains(param, ":") || strings.Contains(param, ".") { + return true + } + return false +} + +func (sl *ServerListen) GetName() string { + return "listen" +} + +func (sl *ServerListen) GetBlock() IBlock { + return nil +} + +func (sl *ServerListen) GetParameters() []string { + params := []string{sl.Bind} + params = append(params, sl.Parameters...) + params = append(params, sl.DefaultServer) + return params +} + +func (sl *ServerListen) GetComment() string { + return sl.Comment +} + +func (sl *ServerListen) AddDefaultServer() { + sl.DefaultServer = DefaultServer +} + +func (sl *ServerListen) RemoveDefaultServe() { + sl.DefaultServer = "" +} + +func (sl *ServerListen) GetLine() int { + return sl.Line +} diff --git a/agent/utils/nginx/components/statement.go b/agent/utils/nginx/components/statement.go new file mode 100644 index 000000000..432a4c165 --- /dev/null +++ b/agent/utils/nginx/components/statement.go @@ -0,0 +1,19 @@ +package components + +type IBlock interface { + GetDirectives() []IDirective + FindDirectives(directiveName string) []IDirective + RemoveDirective(name string, params []string) + UpdateDirective(name string, params []string) + GetComment() string + GetLine() int + GetCodeBlock() string +} + +type IDirective interface { + GetName() string + GetParameters() []string + GetBlock() IBlock + GetComment() string + GetLine() int +} diff --git a/agent/utils/nginx/components/upstream.go b/agent/utils/nginx/components/upstream.go new file mode 100644 index 000000000..68e26d42d --- /dev/null +++ b/agent/utils/nginx/components/upstream.go @@ -0,0 +1,135 @@ +package components + +import ( + "errors" +) + +type Upstream struct { + UpstreamName string + UpstreamServers []*UpstreamServer + Directives []IDirective + Comment string + Line int +} + +func (us *Upstream) GetCodeBlock() string { + return "" +} + +func (us *Upstream) GetName() string { + return "upstream" +} + +func (us *Upstream) GetParameters() []string { + return []string{us.UpstreamName} +} + +func (us *Upstream) GetBlock() IBlock { + return us +} + +func (us *Upstream) GetComment() string { + return us.Comment +} + +func (us *Upstream) GetDirectives() []IDirective { + directives := make([]IDirective, 0) + directives = append(directives, us.Directives...) + for _, uss := range us.UpstreamServers { + directives = append(directives, uss) + } + return directives +} + +func NewUpstream(directive IDirective) (*Upstream, error) { + parameters := directive.GetParameters() + us := &Upstream{ + UpstreamName: parameters[0], + Line: directive.GetLine(), + } + + if block := directive.GetBlock(); block != nil { + us.Comment = block.GetComment() + for _, d := range block.GetDirectives() { + if d.GetName() == "server" { + us.UpstreamServers = append(us.UpstreamServers, NewUpstreamServer(d)) + } else { + us.Directives = append(us.Directives, d) + } + } + return us, nil + } + + return nil, errors.New("missing upstream block") +} + +func (us *Upstream) AddServer(server *UpstreamServer) { + us.UpstreamServers = append(us.UpstreamServers, server) +} + +func (us *Upstream) FindDirectives(directiveName string) []IDirective { + directives := make([]IDirective, 0) + for _, directive := range us.Directives { + if directive.GetName() == directiveName { + directives = append(directives, directive) + } + if directive.GetBlock() != nil { + directives = append(directives, directive.GetBlock().FindDirectives(directiveName)...) + } + } + + return directives +} + +func (us *Upstream) UpdateDirective(key string, params []string) { + if key == "" || len(params) == 0 { + return + } + directives := us.GetDirectives() + index := -1 + for i, dir := range directives { + if dir.GetName() == key { + if IsRepeatKey(key) { + oldParams := dir.GetParameters() + if !(len(oldParams) > 0 && oldParams[0] == params[0]) { + continue + } + } + index = i + break + } + } + newDirective := &Directive{ + Name: key, + Parameters: params, + } + if index > -1 { + directives[index] = newDirective + } else { + directives = append(directives, newDirective) + } + us.Directives = directives +} + +func (us *Upstream) RemoveDirective(key string, params []string) { + directives := us.GetDirectives() + var newDirectives []IDirective + for _, dir := range directives { + if dir.GetName() == key { + if IsRepeatKey(key) && len(params) > 0 { + oldParams := dir.GetParameters() + if oldParams[0] == params[0] { + continue + } + } else { + continue + } + } + newDirectives = append(newDirectives, dir) + } + us.Directives = newDirectives +} + +func (us *Upstream) GetLine() int { + return us.Line +} diff --git a/agent/utils/nginx/components/upstream_server.go b/agent/utils/nginx/components/upstream_server.go new file mode 100644 index 000000000..f5af46493 --- /dev/null +++ b/agent/utils/nginx/components/upstream_server.go @@ -0,0 +1,83 @@ +package components + +import ( + "fmt" + "sort" + "strings" +) + +type UpstreamServer struct { + Comment string + Address string + Flags []string + Parameters map[string]string + Line int +} + +func (uss *UpstreamServer) GetName() string { + return "server" +} + +func (uss *UpstreamServer) GetBlock() IBlock { + return nil +} + +func (uss *UpstreamServer) GetParameters() []string { + return uss.GetDirective().Parameters +} + +func (uss *UpstreamServer) GetComment() string { + return uss.Comment +} + +func (uss *UpstreamServer) GetDirective() *Directive { + directive := &Directive{ + Name: "server", + Parameters: make([]string, 0), + Block: nil, + } + + directive.Parameters = append(directive.Parameters, uss.Address) + + paramNames := make([]string, 0) + for k := range uss.Parameters { + paramNames = append(paramNames, k) + } + sort.Strings(paramNames) + + for _, k := range paramNames { + directive.Parameters = append(directive.Parameters, fmt.Sprintf("%s=%s", k, uss.Parameters[k])) + } + + directive.Parameters = append(directive.Parameters, uss.Flags...) + + return directive +} + +func NewUpstreamServer(directive IDirective) *UpstreamServer { + uss := &UpstreamServer{ + Comment: directive.GetComment(), + Flags: make([]string, 0), + Parameters: make(map[string]string, 0), + Line: directive.GetLine(), + } + + for i, parameter := range directive.GetParameters() { + if i == 0 { + uss.Address = parameter + continue + } + if strings.Contains(parameter, "=") { + s := strings.SplitN(parameter, "=", 2) + uss.Parameters[s[0]] = s[1] + } else { + uss.Flags = append(uss.Flags, parameter) + } + } + + return uss +} + +func (uss *UpstreamServer) GetLine() int { + return uss.Line +} diff --git a/agent/utils/nginx/dumper.go b/agent/utils/nginx/dumper.go new file mode 100644 index 000000000..ba348d156 --- /dev/null +++ b/agent/utils/nginx/dumper.go @@ -0,0 +1,115 @@ +package nginx + +import ( + "bytes" + "fmt" + "os" + "strings" + + "github.com/1Panel-dev/1Panel/agent/utils/nginx/components" +) + +var ( + IndentedStyle = &Style{ + SpaceBeforeBlocks: false, + StartIndent: 0, + Indent: 4, + } +) + +type Style struct { + SpaceBeforeBlocks bool + StartIndent int + Indent int +} + +func (s *Style) Iterate() *Style { + newStyle := &Style{ + SpaceBeforeBlocks: s.SpaceBeforeBlocks, + StartIndent: s.StartIndent + s.Indent, + Indent: s.Indent, + } + return newStyle +} + +func DumpDirective(d components.IDirective, style *Style) string { + var buf bytes.Buffer + + if style.SpaceBeforeBlocks && d.GetBlock() != nil { + buf.WriteString("\n") + } + buf.WriteString(fmt.Sprintf("%s%s", strings.Repeat(" ", style.StartIndent), d.GetName())) + if len(d.GetParameters()) > 0 { + buf.WriteString(fmt.Sprintf(" %s", strings.Join(d.GetParameters(), " "))) + } + if d.GetBlock() == nil { + if d.GetName() != "" { + buf.WriteRune(';') + buf.WriteString(" ") + } + if d.GetComment() != "" { + buf.WriteString(d.GetComment()) + } + } else { + buf.WriteString(" {") + if d.GetComment() != "" { + buf.WriteString(" ") + buf.WriteString(d.GetComment()) + } + buf.WriteString("\n") + buf.WriteString(DumpBlock(d.GetBlock(), style.Iterate(), d.GetBlock().GetLine())) + buf.WriteString(fmt.Sprintf("\n%s}", strings.Repeat(" ", style.StartIndent))) + } + return buf.String() +} + +func DumpBlock(b components.IBlock, style *Style, startLine int) string { + var buf bytes.Buffer + + if b.GetCodeBlock() != "" { + luaLines := strings.Split(b.GetCodeBlock(), "\n") + for i, line := range luaLines { + if strings.Replace(line, " ", "", -1) == "" { + continue + } + buf.WriteString(line) + if i != len(luaLines)-1 { + buf.WriteString("\n") + } + } + return buf.String() + } + + line := startLine + if b.GetLine() > startLine { + for i := 0; i < b.GetLine()-startLine; i++ { + buf.WriteString("\n") + } + line = b.GetLine() + } + + directives := b.GetDirectives() + for i, directive := range directives { + + if directive.GetLine() > line { + for i := 0; i < b.GetLine()-line; i++ { + buf.WriteString("\n") + } + line = b.GetLine() + } + + buf.WriteString(DumpDirective(directive, style)) + if i != len(directives)-1 { + buf.WriteString("\n") + } + } + return buf.String() +} + +func DumpConfig(c *components.Config, style *Style) string { + return DumpBlock(c.Block, style, 1) +} + +func WriteConfig(c *components.Config, style *Style) error { + return os.WriteFile(c.FilePath, []byte(DumpConfig(c, style)), 0644) +} diff --git a/agent/utils/nginx/parser/flag/flag.go b/agent/utils/nginx/parser/flag/flag.go new file mode 100644 index 000000000..b39d9e7ac --- /dev/null +++ b/agent/utils/nginx/parser/flag/flag.go @@ -0,0 +1,59 @@ +package flag + +type Type int + +const ( + EOF Type = iota + Eol + Keyword + QuotedString + Variable + BlockStart + BlockEnd + Semicolon + Comment + Illegal + Regex + LuaCode +) + +var ( + FlagName = map[Type]string{ + QuotedString: "QuotedString", + EOF: "Eof", + Keyword: "Keyword", + Variable: "Variable", + BlockStart: "BlockStart", + BlockEnd: "BlockEnd", + Semicolon: "Semicolon", + Comment: "Comment", + Illegal: "Illegal", + Regex: "Regex", + } +) + +func (tt Type) String() string { + return FlagName[tt] +} + +type Flag struct { + Type Type + Literal string + Line int + Column int +} + +func (t Flag) Lit(literal string) Flag { + t.Literal = literal + return t +} + +type Flags []Flag + +func (t Flag) Is(typ Type) bool { + return t.Type == typ +} + +func (t Flag) IsParameterEligible() bool { + return t.Is(Keyword) || t.Is(QuotedString) || t.Is(Variable) || t.Is(Regex) +} diff --git a/agent/utils/nginx/parser/lexer.go b/agent/utils/nginx/parser/lexer.go new file mode 100644 index 000000000..c0648f81b --- /dev/null +++ b/agent/utils/nginx/parser/lexer.go @@ -0,0 +1,262 @@ +package parser + +import ( + "bufio" + "bytes" + "io" + "strings" + + "github.com/1Panel-dev/1Panel/agent/utils/nginx/parser/flag" +) + +type lexer struct { + reader *bufio.Reader + file string + line int + column int + inLuaBlock bool + Latest flag.Flag +} + +func lex(content string) *lexer { + return newLexer(bytes.NewBuffer([]byte(content))) +} + +func newLexer(r io.Reader) *lexer { + return &lexer{ + line: 1, + reader: bufio.NewReader(r), + } +} + +func (s *lexer) scan() flag.Flag { + s.Latest = s.getNextFlag() + return s.Latest +} + +//func (s *lexer) all() flag.Flags { +// tokens := make([]flag.Flag, 0) +// for { +// v := s.scan() +// if v.Type == flag.EOF || v.Type == -1 { +// break +// } +// tokens = append(tokens, v) +// } +// return tokens +//} + +func (s *lexer) getNextFlag() flag.Flag { + if s.inLuaBlock { + s.inLuaBlock = false + flag := s.scanLuaCode() + return flag + } +retoFlag: + ch := s.peek() + switch { + case isSpace(ch): + s.skipWhitespace() + goto retoFlag + case isEOF(ch): + return s.NewToken(flag.EOF).Lit(string(s.read())) + case ch == ';': + return s.NewToken(flag.Semicolon).Lit(string(s.read())) + case ch == '{': + if isLuaBlock(s.Latest) { + s.inLuaBlock = true + } + return s.NewToken(flag.BlockStart).Lit(string(s.read())) + case ch == '}': + return s.NewToken(flag.BlockEnd).Lit(string(s.read())) + case ch == '#': + return s.scanComment() + case ch == '$': + return s.scanVariable() + case isQuote(ch): + return s.scanQuotedString(ch) + default: + return s.scanKeyword() + } +} + +func (s *lexer) scanLuaCode() flag.Flag { + ret := s.NewToken(flag.LuaCode) + stack := make([]rune, 0, 50) + code := strings.Builder{} + + for { + ch := s.read() + if ch == rune(flag.EOF) { + panic("unexpected end of file while scanning a string, maybe an unclosed lua code?") + } + if ch == '#' { + code.WriteRune(ch) + code.WriteString(s.readUntil(isEndOfLine)) + continue + } else if ch == '}' { + if len(stack) == 0 { + _ = s.reader.UnreadRune() + return ret.Lit(strings.TrimRight(strings.Trim(code.String(), "\n"), "\n ")) + } + if stack[len(stack)-1] == '{' { + stack = stack[0 : len(stack)-1] + } + } else if ch == '{' { + stack = append(stack, ch) + } + code.WriteRune(ch) + } +} + +func (s *lexer) peek() rune { + r, _, _ := s.reader.ReadRune() + _ = s.reader.UnreadRune() + return r +} + +type runeCheck func(rune) bool + +func (s *lexer) readUntil(until runeCheck) string { + var buf bytes.Buffer + buf.WriteRune(s.read()) + + for { + if ch := s.peek(); isEOF(ch) { + break + } else if until(ch) { + break + } else { + buf.WriteRune(s.read()) + } + } + + return buf.String() +} + +func (s *lexer) NewToken(tokenType flag.Type) flag.Flag { + return flag.Flag{ + Type: tokenType, + Line: s.line, + Column: s.column, + } +} + +func (s *lexer) readWhile(while runeCheck) string { + var buf bytes.Buffer + buf.WriteRune(s.read()) + + for { + if ch := s.peek(); while(ch) { + buf.WriteRune(s.read()) + } else { + break + } + } + return buf.String() +} + +func (s *lexer) skipWhitespace() { + s.readWhile(isSpace) +} + +func (s *lexer) scanComment() flag.Flag { + return s.NewToken(flag.Comment).Lit(s.readUntil(isEndOfLine)) +} + +func (s *lexer) scanQuotedString(delimiter rune) flag.Flag { + var buf bytes.Buffer + tok := s.NewToken(flag.QuotedString) + _, _ = buf.WriteRune(s.read()) + for { + ch := s.read() + + if ch == rune(flag.EOF) { + panic("unexpected end of file while scanning a string, maybe an unclosed quote?") + } + + if ch == '\\' && (s.peek() == delimiter) { + buf.WriteRune(ch) + buf.WriteRune(s.read()) + continue + } + + _, _ = buf.WriteRune(ch) + if ch == delimiter { + break + } + } + + return tok.Lit(buf.String()) +} + +func (s *lexer) scanKeyword() flag.Flag { + var buf bytes.Buffer + tok := s.NewToken(flag.Keyword) + prev := s.read() + buf.WriteRune(prev) + for { + ch := s.peek() + + if isSpace(ch) || isEOF(ch) || ch == ';' { + break + } + + if ch == '{' { + if prev == '$' { + buf.WriteString(s.readUntil(func(r rune) bool { + return r == '}' + })) + buf.WriteRune(s.read()) //consume latest '}' + } else { + break + } + } + buf.WriteRune(s.read()) + } + + return tok.Lit(buf.String()) +} + +func (s *lexer) scanVariable() flag.Flag { + return s.NewToken(flag.Variable).Lit(s.readUntil(isKeywordTerminator)) +} + +func (s *lexer) read() rune { + ch, _, err := s.reader.ReadRune() + if err != nil { + return rune(flag.EOF) + } + + if ch == '\n' { + s.column = 1 + s.line++ + } else { + s.column++ + } + return ch +} + +func isQuote(ch rune) bool { + return ch == '"' || ch == '\'' || ch == '`' +} + +func isKeywordTerminator(ch rune) bool { + return isSpace(ch) || isEndOfLine(ch) || ch == '{' || ch == ';' +} + +func isSpace(ch rune) bool { + return ch == ' ' || ch == '\t' || isEndOfLine(ch) +} + +func isEOF(ch rune) bool { + return ch == rune(flag.EOF) +} + +func isEndOfLine(ch rune) bool { + return ch == '\r' || ch == '\n' +} + +func isLuaBlock(t flag.Flag) bool { + return t.Type == flag.Keyword && strings.HasSuffix(t.Literal, "_by_lua_block") +} diff --git a/agent/utils/nginx/parser/parser.go b/agent/utils/nginx/parser/parser.go new file mode 100644 index 000000000..cdf82101c --- /dev/null +++ b/agent/utils/nginx/parser/parser.go @@ -0,0 +1,210 @@ +package parser + +import ( + "bufio" + "errors" + "fmt" + "os" + "strings" + + components "github.com/1Panel-dev/1Panel/agent/utils/nginx/components" + "github.com/1Panel-dev/1Panel/agent/utils/nginx/parser/flag" +) + +type Parser struct { + lexer *lexer + currentToken flag.Flag + followingToken flag.Flag + blockWrappers map[string]func(*components.Directive) (components.IDirective, error) + directiveWrappers map[string]func(*components.Directive) components.IDirective +} + +func NewStringParser(str string) *Parser { + return NewParserFromLexer(lex(str)) +} + +func NewParser(filePath string) (*Parser, error) { + f, err := os.Open(filePath) + if err != nil { + return nil, err + } + defer f.Close() + l := newLexer(bufio.NewReader(f)) + l.file = filePath + p := NewParserFromLexer(l) + return p, nil +} + +func NewParserFromLexer(lexer *lexer) *Parser { + parser := &Parser{ + lexer: lexer, + } + + parser.nextToken() + parser.nextToken() + + parser.blockWrappers = map[string]func(*components.Directive) (components.IDirective, error){ + "http": func(directive *components.Directive) (components.IDirective, error) { + return parser.wrapHttp(directive), nil + }, + "server": func(directive *components.Directive) (components.IDirective, error) { + return parser.wrapServer(directive), nil + }, + "location": func(directive *components.Directive) (components.IDirective, error) { + return parser.wrapLocation(directive), nil + }, + "upstream": func(directive *components.Directive) (components.IDirective, error) { + return parser.wrapUpstream(directive), nil + }, + "_by_lua_block": func(directive *components.Directive) (components.IDirective, error) { + return parser.wrapLuaBlock(directive) + }, + } + + parser.directiveWrappers = map[string]func(*components.Directive) components.IDirective{ + "server": func(directive *components.Directive) components.IDirective { + return parser.parseUpstreamServer(directive) + }, + } + + return parser +} + +func (p *Parser) nextToken() { + p.currentToken = p.followingToken + p.followingToken = p.lexer.scan() +} + +func (p *Parser) curTokenIs(t flag.Type) bool { + return p.currentToken.Type == t +} + +func (p *Parser) followingTokenIs(t flag.Type) bool { + return p.followingToken.Type == t +} + +func (p *Parser) Parse() (*components.Config, error) { + parsedBlock, err := p.parseBlock(false) + if err != nil { + return nil, err + } + c := &components.Config{ + FilePath: p.lexer.file, + Block: parsedBlock, + } + return c, err +} + +func (p *Parser) parseBlock(inBlock bool) (*components.Block, error) { + context := &components.Block{ + Comment: "", + Directives: make([]components.IDirective, 0), + Line: p.currentToken.Line, + } + +parsingloop: + for { + switch { + case p.curTokenIs(flag.EOF): + if inBlock { + return nil, errors.New("unexpected eof in block") + } + break parsingloop + case p.curTokenIs(flag.BlockEnd): + break parsingloop + case p.curTokenIs(flag.LuaCode): + context.IsLuaBlock = true + context.LiteralCode = p.currentToken.Literal + case p.curTokenIs(flag.Keyword) || p.curTokenIs(flag.QuotedString): + s, err := p.parseStatement() + if err != nil { + return nil, err + } + context.Directives = append(context.Directives, s) + case p.curTokenIs(flag.Comment): + context.Directives = append(context.Directives, &components.Comment{ + Detail: p.currentToken.Literal, + Line: p.currentToken.Line, + }) + } + p.nextToken() + } + + return context, nil +} + +func (p *Parser) parseStatement() (components.IDirective, error) { + d := &components.Directive{ + Name: p.currentToken.Literal, + Line: p.currentToken.Line, + } + + for p.nextToken(); p.currentToken.IsParameterEligible(); p.nextToken() { + d.Parameters = append(d.Parameters, p.currentToken.Literal) + } + + if p.curTokenIs(flag.Semicolon) { + if dw, ok := p.directiveWrappers[d.Name]; ok { + return dw(d), nil + } + if p.followingTokenIs(flag.Comment) && p.currentToken.Line == p.followingToken.Line { + d.Comment = p.followingToken.Literal + p.nextToken() + } + return d, nil + } + + if p.curTokenIs(flag.BlockStart) { + inLineComment := "" + if p.followingTokenIs(flag.Comment) && p.currentToken.Line == p.followingToken.Line { + inLineComment = p.followingToken.Literal + p.nextToken() + p.nextToken() + } + block, err := p.parseBlock(false) + if err != nil { + return nil, err + } + + block.Comment = inLineComment + d.Block = block + + if strings.HasSuffix(d.Name, "_by_lua_block") { + return p.blockWrappers["_by_lua_block"](d) + } + + if bw, ok := p.blockWrappers[d.Name]; ok { + return bw(d) + } + return d, nil + } + + panic(fmt.Errorf("unexpected token %s (%s) on line %d, column %d", p.currentToken.Type.String(), p.currentToken.Literal, p.currentToken.Line, p.currentToken.Column)) +} + +func (p *Parser) wrapLocation(directive *components.Directive) *components.Location { + return components.NewLocation(directive) +} + +func (p *Parser) wrapServer(directive *components.Directive) *components.Server { + s, _ := components.NewServer(directive) + return s +} + +func (p *Parser) wrapUpstream(directive *components.Directive) *components.Upstream { + s, _ := components.NewUpstream(directive) + return s +} + +func (p *Parser) wrapHttp(directive *components.Directive) *components.Http { + h, _ := components.NewHttp(directive) + return h +} + +func (p *Parser) wrapLuaBlock(directive *components.Directive) (*components.LuaBlock, error) { + return components.NewLuaBlock(directive) +} + +func (p *Parser) parseUpstreamServer(directive *components.Directive) *components.UpstreamServer { + return components.NewUpstreamServer(directive) +} diff --git a/agent/utils/ntp/ntp.go b/agent/utils/ntp/ntp.go new file mode 100644 index 000000000..914b22962 --- /dev/null +++ b/agent/utils/ntp/ntp.go @@ -0,0 +1,84 @@ +package ntp + +import ( + "encoding/binary" + "fmt" + "net" + "runtime" + "time" + + "github.com/1Panel-dev/1Panel/agent/utils/cmd" +) + +const ntpEpochOffset = 2208988800 + +type packet struct { + Settings uint8 + Stratum uint8 + Poll int8 + Precision int8 + RootDelay uint32 + RootDispersion uint32 + ReferenceID uint32 + RefTimeSec uint32 + RefTimeFrac uint32 + OrigTimeSec uint32 + OrigTimeFrac uint32 + RxTimeSec uint32 + RxTimeFrac uint32 + TxTimeSec uint32 + TxTimeFrac uint32 +} + +func GetRemoteTime(site string) (time.Time, error) { + conn, err := net.Dial("udp", site+":123") + if err != nil { + return time.Time{}, fmt.Errorf("failed to connect: %v", err) + } + defer conn.Close() + if err := conn.SetDeadline(time.Now().Add(15 * time.Second)); err != nil { + return time.Time{}, fmt.Errorf("failed to set deadline: %v", err) + } + + req := &packet{Settings: 0x1B} + + if err := binary.Write(conn, binary.BigEndian, req); err != nil { + return time.Time{}, fmt.Errorf("failed to set request: %v", err) + } + + rsp := &packet{} + if err := binary.Read(conn, binary.BigEndian, rsp); err != nil { + return time.Time{}, fmt.Errorf("failed to read server response: %v", err) + } + + secs := float64(rsp.TxTimeSec) - ntpEpochOffset + nanos := (int64(rsp.TxTimeFrac) * 1e9) >> 32 + + showtime := time.Unix(int64(secs), nanos) + + return showtime, nil +} + +func UpdateSystemTime(dateTime string) error { + system := runtime.GOOS + if system == "linux" { + stdout2, err := cmd.Execf(`%s date -s "%s"`, cmd.SudoHandleCmd(), dateTime) + if err != nil { + return fmt.Errorf("update system time failed,stdout: %s, err: %v", stdout2, err) + } + return nil + } + return fmt.Errorf("the current system architecture %v does not support synchronization", system) +} + +func UpdateSystemTimeZone(timezone string) error { + system := runtime.GOOS + if system == "linux" { + stdout, err := cmd.Execf(`%s timedatectl set-timezone "%s"`, cmd.SudoHandleCmd(), timezone) + if err != nil { + return fmt.Errorf("update system time zone failed, stdout: %s, err: %v", stdout, err) + } + return nil + } + return fmt.Errorf("the current system architecture %v does not support synchronization", system) +} diff --git a/agent/utils/postgresql/client.go b/agent/utils/postgresql/client.go new file mode 100644 index 000000000..f6d3e13c0 --- /dev/null +++ b/agent/utils/postgresql/client.go @@ -0,0 +1,59 @@ +package postgresql + +import ( + "context" + "database/sql" + "errors" + "fmt" + "time" + + "github.com/1Panel-dev/1Panel/agent/buserr" + "github.com/1Panel-dev/1Panel/agent/constant" + "github.com/1Panel-dev/1Panel/agent/utils/postgresql/client" + _ "github.com/jackc/pgx/v5/stdlib" +) + +type PostgresqlClient interface { + Create(info client.CreateInfo) error + CreateUser(info client.CreateInfo, withDeleteDB bool) error + Delete(info client.DeleteInfo) error + ChangePrivileges(info client.Privileges) error + ChangePassword(info client.PasswordChangeInfo) error + + Backup(info client.BackupInfo) error + Recover(info client.RecoverInfo) error + SyncDB() ([]client.SyncDBInfo, error) + Close() +} + +func NewPostgresqlClient(conn client.DBInfo) (PostgresqlClient, error) { + if conn.From == "local" { + connArgs := []string{"exec", conn.Address, "psql", "-t", "-U", conn.Username, "-c"} + return client.NewLocal(connArgs, conn.Address, conn.Username, conn.Password, conn.Database), nil + } + + connArgs := fmt.Sprintf("postgres://%s:%s@%s:%d/?sslmode=disable", conn.Username, conn.Password, conn.Address, conn.Port) + db, err := sql.Open("pgx", connArgs) + if err != nil { + return nil, err + } + + ctx, cancel := context.WithTimeout(context.Background(), time.Duration(conn.Timeout)*time.Second) + defer cancel() + if err := db.PingContext(ctx); err != nil { + return nil, err + } + if errors.Is(ctx.Err(), context.DeadlineExceeded) { + return nil, buserr.New(constant.ErrExecTimeOut) + } + + return client.NewRemote(client.Remote{ + Client: db, + From: "remote", + Database: conn.Database, + User: conn.Username, + Password: conn.Password, + Address: conn.Address, + Port: conn.Port, + }), nil +} diff --git a/agent/utils/postgresql/client/info.go b/agent/utils/postgresql/client/info.go new file mode 100644 index 000000000..b1b789dcf --- /dev/null +++ b/agent/utils/postgresql/client/info.go @@ -0,0 +1,81 @@ +package client + +import ( + _ "github.com/jackc/pgx/v5/stdlib" +) + +type DBInfo struct { + From string `json:"from"` + Database string `json:"database"` + Address string `json:"address"` + Port uint `json:"port"` + Username string `json:"userName"` + Password string `json:"password"` + + Timeout uint `json:"timeout"` // second +} + +type CreateInfo struct { + Name string `json:"name"` + Username string `json:"userName"` + Password string `json:"password"` + SuperUser bool `json:"superUser"` + + Timeout uint `json:"timeout"` // second +} + +type Privileges struct { + Username string `json:"userName"` + SuperUser bool `json:"superUser"` + + Timeout uint `json:"timeout"` // second +} + +type DeleteInfo struct { + Name string `json:"name"` + Username string `json:"userName"` + + ForceDelete bool `json:"forceDelete"` + Timeout uint `json:"timeout"` // second +} + +type PasswordChangeInfo struct { + Username string `json:"userName"` + Password string `json:"password"` + + Timeout uint `json:"timeout"` // second +} + +type BackupInfo struct { + Name string `json:"name"` + TargetDir string `json:"targetDir"` + FileName string `json:"fileName"` + + Timeout uint `json:"timeout"` // second +} + +type RecoverInfo struct { + Name string `json:"name"` + SourceFile string `json:"sourceFile"` + Username string `json:"username"` + + Timeout uint `json:"timeout"` // second +} + +type SyncDBInfo struct { + Name string `json:"name"` + From string `json:"from"` + PostgresqlName string `json:"postgresqlName"` +} +type Status struct { + Uptime string `json:"uptime"` + Version string `json:"version"` + MaxConnections string `json:"max_connections"` + Autovacuum string `json:"autovacuum"` + CurrentConnections string `json:"current_connections"` + HitRatio string `json:"hit_ratio"` + SharedBuffers string `json:"shared_buffers"` + BuffersClean string `json:"buffers_clean"` + MaxwrittenClean string `json:"maxwritten_clean"` + BuffersBackendFsync string `json:"buffers_backend_fsync"` +} diff --git a/agent/utils/postgresql/client/local.go b/agent/utils/postgresql/client/local.go new file mode 100644 index 000000000..ca3604977 --- /dev/null +++ b/agent/utils/postgresql/client/local.go @@ -0,0 +1,230 @@ +package client + +import ( + "bytes" + "compress/gzip" + "context" + "errors" + "fmt" + "os" + "os/exec" + "path" + "strings" + "time" + + "github.com/1Panel-dev/1Panel/agent/buserr" + "github.com/1Panel-dev/1Panel/agent/constant" + "github.com/1Panel-dev/1Panel/agent/global" + "github.com/1Panel-dev/1Panel/agent/utils/files" +) + +type Local struct { + PrefixCommand []string + Database string + Username string + Password string + ContainerName string +} + +func NewLocal(command []string, containerName, username, password, database string) *Local { + return &Local{PrefixCommand: command, ContainerName: containerName, Username: username, Password: password, Database: database} +} + +func (r *Local) Create(info CreateInfo) error { + createSql := fmt.Sprintf("CREATE DATABASE \"%s\"", info.Name) + if err := r.ExecSQL(createSql, info.Timeout); err != nil { + if strings.Contains(strings.ToLower(err.Error()), "already exists") { + return buserr.New(constant.ErrDatabaseIsExist) + } + return err + } + + if err := r.CreateUser(info, true); err != nil { + _ = r.ExecSQL(fmt.Sprintf("DROP DATABASE \"%s\"", info.Name), info.Timeout) + return err + } + + return nil +} + +func (r *Local) ChangePrivileges(info Privileges) error { + super := "SUPERUSER" + if !info.SuperUser { + super = "NOSUPERUSER" + } + changeSql := fmt.Sprintf("ALTER USER \"%s\" WITH %s", info.Username, super) + return r.ExecSQL(changeSql, info.Timeout) +} + +func (r *Local) CreateUser(info CreateInfo, withDeleteDB bool) error { + createSql := fmt.Sprintf("CREATE USER \"%s\" WITH PASSWORD '%s'", info.Username, info.Password) + if err := r.ExecSQL(createSql, info.Timeout); err != nil { + if strings.Contains(strings.ToLower(err.Error()), "already exists") { + return buserr.New(constant.ErrUserIsExist) + } + if withDeleteDB { + _ = r.Delete(DeleteInfo{ + Name: info.Name, + Username: info.Username, + ForceDelete: true, + Timeout: 300}) + } + return err + } + if info.SuperUser { + if err := r.ChangePrivileges(Privileges{SuperUser: true, Username: info.Username, Timeout: info.Timeout}); err != nil { + if withDeleteDB { + _ = r.Delete(DeleteInfo{ + Name: info.Name, + Username: info.Username, + ForceDelete: true, + Timeout: 300}) + } + return err + } + } + grantStr := fmt.Sprintf("GRANT ALL PRIVILEGES ON DATABASE \"%s\" TO \"%s\"", info.Name, info.Username) + if err := r.ExecSQL(grantStr, info.Timeout); err != nil { + if withDeleteDB { + _ = r.Delete(DeleteInfo{ + Name: info.Name, + Username: info.Username, + ForceDelete: true, + Timeout: 300}) + } + return err + } + return nil +} + +func (r *Local) Delete(info DeleteInfo) error { + if len(info.Name) != 0 { + dropSql := fmt.Sprintf("DROP DATABASE \"%s\"", info.Name) + if err := r.ExecSQL(dropSql, info.Timeout); err != nil && !info.ForceDelete { + return err + } + } + dropSql := fmt.Sprintf("DROP USER \"%s\"", info.Username) + if err := r.ExecSQL(dropSql, info.Timeout); err != nil && !info.ForceDelete { + if strings.Contains(strings.ToLower(err.Error()), "depend on it") { + return buserr.WithDetail(constant.ErrInUsed, info.Username, nil) + } + return err + } + return nil +} + +func (r *Local) ChangePassword(info PasswordChangeInfo) error { + changeSql := fmt.Sprintf("ALTER USER \"%s\" WITH PASSWORD '%s'", info.Username, info.Password) + if err := r.ExecSQL(changeSql, info.Timeout); err != nil { + return err + } + + return nil +} + +func (r *Local) Backup(info BackupInfo) error { + fileOp := files.NewFileOp() + if !fileOp.Stat(info.TargetDir) { + if err := os.MkdirAll(info.TargetDir, os.ModePerm); err != nil { + return fmt.Errorf("mkdir %s failed, err: %v", info.TargetDir, err) + } + } + outfile, err := os.OpenFile(path.Join(info.TargetDir, info.FileName), os.O_RDWR|os.O_CREATE, 0755) + if err != nil { + return fmt.Errorf("open file %s failed, err: %v", path.Join(info.TargetDir, info.FileName), err) + } + defer outfile.Close() + global.LOG.Infof("start to pg_dump | gzip > %s.gzip", info.TargetDir+"/"+info.FileName) + cmd := exec.Command("docker", "exec", r.ContainerName, "pg_dump", "-F", "c", "-U", r.Username, "-d", info.Name) + var stderr bytes.Buffer + cmd.Stderr = &stderr + + gzipCmd := exec.Command("gzip", "-cf") + gzipCmd.Stdin, _ = cmd.StdoutPipe() + gzipCmd.Stdout = outfile + _ = gzipCmd.Start() + + if err := cmd.Run(); err != nil { + return fmt.Errorf("handle backup database failed, err: %v", stderr.String()) + } + _ = gzipCmd.Wait() + return nil +} + +func (r *Local) Recover(info RecoverInfo) error { + fi, _ := os.Open(info.SourceFile) + defer fi.Close() + cmd := exec.Command("docker", "exec", "-i", r.ContainerName, "pg_restore", "-F", "c", "-c", "-U", r.Username, "-d", info.Name) + 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() + if err != nil || strings.HasPrefix(string(stdout), "ERROR ") { + return errors.New(string(stdout)) + } + + return nil +} + +func (r *Local) SyncDB() ([]SyncDBInfo, error) { + var datas []SyncDBInfo + lines, err := r.ExecSQLForRows("SELECT datname FROM pg_database", 300) + if err != nil { + return datas, err + } + for _, line := range lines { + itemLine := strings.TrimLeft(line, " ") + if len(itemLine) == 0 || itemLine == "postgres" || itemLine == "template1" || itemLine == "template0" || itemLine == r.Username { + continue + } + datas = append(datas, SyncDBInfo{Name: itemLine, From: "local", PostgresqlName: r.Database}) + } + return datas, nil +} + +func (r *Local) Close() {} + +func (r *Local) ExecSQL(command string, timeout uint) error { + itemCommand := r.PrefixCommand[:] + itemCommand = append(itemCommand, command) + ctx, cancel := context.WithTimeout(context.Background(), time.Duration(timeout)*time.Second) + defer cancel() + cmd := exec.CommandContext(ctx, "docker", itemCommand...) + stdout, err := cmd.CombinedOutput() + if errors.Is(ctx.Err(), context.DeadlineExceeded) { + return buserr.New(constant.ErrExecTimeOut) + } + if err != nil || strings.HasPrefix(string(stdout), "ERROR ") { + return errors.New(string(stdout)) + } + return nil +} + +func (r *Local) ExecSQLForRows(command string, timeout uint) ([]string, error) { + itemCommand := r.PrefixCommand[:] + itemCommand = append(itemCommand, command) + ctx, cancel := context.WithTimeout(context.Background(), time.Duration(timeout)*time.Second) + defer cancel() + cmd := exec.CommandContext(ctx, "docker", itemCommand...) + stdout, err := cmd.CombinedOutput() + if errors.Is(ctx.Err(), context.DeadlineExceeded) { + return nil, buserr.New(constant.ErrExecTimeOut) + } + if err != nil || strings.HasPrefix(string(stdout), "ERROR ") { + return nil, errors.New(string(stdout)) + } + return strings.Split(string(stdout), "\n"), nil +} diff --git a/agent/utils/postgresql/client/remote.go b/agent/utils/postgresql/client/remote.go new file mode 100644 index 000000000..4ffdd57c9 --- /dev/null +++ b/agent/utils/postgresql/client/remote.go @@ -0,0 +1,311 @@ +package client + +import ( + "bufio" + "context" + "database/sql" + "fmt" + "io" + "os" + "os/exec" + "strings" + "time" + + "github.com/1Panel-dev/1Panel/agent/app/model" + "github.com/1Panel-dev/1Panel/agent/global" + "github.com/docker/docker/api/types/image" + "github.com/pkg/errors" + + "github.com/1Panel-dev/1Panel/agent/buserr" + "github.com/1Panel-dev/1Panel/agent/constant" + "github.com/1Panel-dev/1Panel/agent/utils/docker" + "github.com/1Panel-dev/1Panel/agent/utils/files" + _ "github.com/jackc/pgx/v5/stdlib" +) + +type Remote struct { + Client *sql.DB + From string + Database string + User string + Password string + Address string + Port uint +} + +func NewRemote(db Remote) *Remote { + return &db +} +func (r *Remote) Create(info CreateInfo) error { + createSql := fmt.Sprintf("CREATE DATABASE \"%s\"", info.Name) + if err := r.ExecSQL(createSql, info.Timeout); err != nil { + if strings.Contains(strings.ToLower(err.Error()), "already exists") { + return buserr.New(constant.ErrDatabaseIsExist) + } + return err + } + if err := r.CreateUser(info, true); err != nil { + return err + } + return nil +} + +func (r *Remote) CreateUser(info CreateInfo, withDeleteDB bool) error { + createSql := fmt.Sprintf("CREATE USER \"%s\" WITH PASSWORD '%s'", info.Username, info.Password) + if err := r.ExecSQL(createSql, info.Timeout); err != nil { + if strings.Contains(strings.ToLower(err.Error()), "already exists") { + return buserr.New(constant.ErrUserIsExist) + } + if withDeleteDB { + _ = r.Delete(DeleteInfo{ + Name: info.Name, + Username: info.Username, + ForceDelete: true, + Timeout: 300}) + } + return err + } + if info.SuperUser { + if err := r.ChangePrivileges(Privileges{SuperUser: true, Username: info.Username, Timeout: info.Timeout}); err != nil { + if withDeleteDB { + _ = r.Delete(DeleteInfo{ + Name: info.Name, + Username: info.Username, + ForceDelete: true, + Timeout: 300}) + } + return err + } + } + grantSql := fmt.Sprintf("GRANT ALL PRIVILEGES ON DATABASE \"%s\" TO \"%s\"", info.Name, info.Username) + if err := r.ExecSQL(grantSql, info.Timeout); err != nil { + if withDeleteDB { + _ = r.Delete(DeleteInfo{ + Name: info.Name, + Username: info.Username, + ForceDelete: true, + Timeout: 300}) + } + return err + } + + return nil +} + +func (r *Remote) Delete(info DeleteInfo) error { + if len(info.Name) != 0 { + dropSql := fmt.Sprintf("DROP DATABASE \"%s\"", info.Name) + if err := r.ExecSQL(dropSql, info.Timeout); err != nil && !info.ForceDelete { + return err + } + } + dropSql := fmt.Sprintf("DROP USER \"%s\"", info.Username) + if err := r.ExecSQL(dropSql, info.Timeout); err != nil && !info.ForceDelete { + if strings.Contains(strings.ToLower(err.Error()), "depend on it") { + return buserr.WithDetail(constant.ErrInUsed, info.Username, nil) + } + return err + } + return nil +} + +func (r *Remote) ChangePrivileges(info Privileges) error { + super := "SUPERUSER" + if !info.SuperUser { + super = "NOSUPERUSER" + } + return r.ExecSQL(fmt.Sprintf("ALTER USER \"%s\" WITH %s", info.Username, super), info.Timeout) +} + +func (r *Remote) ChangePassword(info PasswordChangeInfo) error { + return r.ExecSQL(fmt.Sprintf("ALTER USER \"%s\" WITH ENCRYPTED PASSWORD '%s'", info.Username, info.Password), info.Timeout) +} + +func (r *Remote) Backup(info BackupInfo) error { + imageTag, err := loadImageTag() + if err != nil { + return err + } + fileOp := files.NewFileOp() + if !fileOp.Stat(info.TargetDir) { + if err := os.MkdirAll(info.TargetDir, os.ModePerm); err != nil { + return fmt.Errorf("mkdir %s failed, err: %v", info.TargetDir, err) + } + } + fileNameItem := info.TargetDir + "/" + strings.TrimSuffix(info.FileName, ".gz") + backupCommand := exec.Command("bash", "-c", + fmt.Sprintf("docker run --rm --net=host -i %s /bin/bash -c 'PGPASSWORD=%s pg_dump -h %s -p %d --no-owner -Fc -U %s %s' > %s", + imageTag, r.Password, r.Address, r.Port, r.User, info.Name, fileNameItem)) + _ = backupCommand.Run() + b := make([]byte, 5) + n := []byte{80, 71, 68, 77, 80} + handle, err := os.OpenFile(fileNameItem, os.O_RDONLY, os.ModePerm) + if err != nil { + return fmt.Errorf("backup file not found,err:%v", err) + } + defer handle.Close() + _, _ = handle.Read(b) + if string(b) != string(n) { + errBytes, _ := os.ReadFile(fileNameItem) + return fmt.Errorf("backup failed,err:%s", string(errBytes)) + } + + 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) + } + return nil +} + +func (r *Remote) Recover(info RecoverInfo) error { + imageTag, err := loadImageTag() + if err != nil { + return err + } + 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() + }() + } + recoverCommand := exec.Command("bash", "-c", + fmt.Sprintf("docker run --rm --net=host -i %s /bin/bash -c 'PGPASSWORD=%s pg_restore -h %s -p %d --verbose --clean --no-privileges --no-owner -Fc -U %s -d %s --role=%s' < %s", + imageTag, r.Password, r.Address, r.Port, r.User, info.Name, info.Username, fileName)) + pipe, _ := recoverCommand.StdoutPipe() + stderrPipe, _ := recoverCommand.StderrPipe() + defer pipe.Close() + defer stderrPipe.Close() + if err := recoverCommand.Start(); err != nil { + return err + } + reader := bufio.NewReader(pipe) + for { + readString, err := reader.ReadString('\n') + if errors.Is(err, io.EOF) { + break + } + if err != nil { + all, _ := io.ReadAll(stderrPipe) + global.LOG.Errorf("[Postgresql] DB:[%s] Recover Error: %s", info.Name, string(all)) + return err + } + global.LOG.Infof("[Postgresql] DB:[%s] Restoring: %s", info.Name, readString) + } + + return nil +} + +func (r *Remote) SyncDB() ([]SyncDBInfo, error) { + ctx, cancel := context.WithTimeout(context.Background(), 300*time.Second) + defer cancel() + + var datas []SyncDBInfo + rows, err := r.Client.Query("SELECT datname FROM pg_database;") + if err != nil { + return nil, err + } + defer rows.Close() + for rows.Next() { + var dbName string + if err := rows.Scan(&dbName); err != nil { + continue + } + if len(dbName) == 0 || dbName == "postgres" || dbName == "template1" || dbName == "template0" || dbName == r.User { + continue + } + datas = append(datas, SyncDBInfo{Name: dbName, From: r.From, PostgresqlName: r.Database}) + } + if errors.Is(ctx.Err(), context.DeadlineExceeded) { + return nil, buserr.New(constant.ErrExecTimeOut) + } + return datas, nil +} + +func (r *Remote) Close() { + _ = r.Client.Close() +} + +func (r *Remote) ExecSQL(command string, timeout uint) error { + ctx, cancel := context.WithTimeout(context.Background(), time.Duration(timeout)*time.Second) + defer cancel() + + if _, err := r.Client.ExecContext(ctx, command); err != nil { + return err + } + if errors.Is(ctx.Err(), context.DeadlineExceeded) { + return buserr.New(constant.ErrExecTimeOut) + } + + return nil +} + +func loadImageTag() (string, error) { + var ( + app model.App + appDetails []model.AppDetail + versions []string + ) + if err := global.DB.Where("key = ?", "postgresql").First(&app).Error; err != nil { + versions = []string{"postgres:16.1-alpine", "postgres:16.0-alpine"} + } else { + if err := global.DB.Where("app_id = ?", app.ID).Find(&appDetails).Error; err != nil { + versions = []string{"postgres:16.1-alpine", "postgres:16.0-alpine"} + } else { + for _, item := range appDetails { + versions = append(versions, "postgres:"+item.Version) + } + } + } + + client, err := docker.NewDockerClient() + if err != nil { + return "", err + } + defer client.Close() + images, err := client.ImageList(context.Background(), image.ListOptions{}) + if err != nil { + return "", err + } + + itemTag := "" + for _, item := range versions { + for _, image := range images { + for _, tag := range image.RepoTags { + if tag == item { + itemTag = tag + break + } + } + if len(itemTag) != 0 { + break + } + } + if len(itemTag) != 0 { + break + } + } + if len(itemTag) != 0 { + return itemTag, nil + } + + itemTag = "postgres:16.1-alpine" + ctx, cancel := context.WithTimeout(context.Background(), 3*time.Minute) + defer cancel() + if _, err := client.ImagePull(ctx, itemTag, image.PullOptions{}); err != nil { + if errors.Is(ctx.Err(), context.DeadlineExceeded) { + return itemTag, buserr.New(constant.ErrPgImagePull) + } + global.LOG.Errorf("image %s pull failed, err: %v", itemTag, err) + return itemTag, fmt.Errorf("image %s pull failed, err: %v", itemTag, err) + } + + return itemTag, nil +} diff --git a/agent/utils/qqwry/qqwry.go b/agent/utils/qqwry/qqwry.go new file mode 100644 index 000000000..bdb1558e9 --- /dev/null +++ b/agent/utils/qqwry/qqwry.go @@ -0,0 +1,165 @@ +package qqwry + +import ( + "encoding/binary" + "net" + "strings" + + "github.com/1Panel-dev/1Panel/agent/cmd/server/qqwry" + "golang.org/x/text/encoding/simplifiedchinese" +) + +const ( + indexLen = 7 + redirectMode1 = 0x01 + redirectMode2 = 0x02 +) + +var IpCommonDictionary []byte + +type QQwry struct { + Data []byte + Offset int64 +} + +func NewQQwry() (*QQwry, error) { + IpCommonDictionary := qqwry.QQwryByte + return &QQwry{Data: IpCommonDictionary}, nil +} + +// readData 从文件中读取数据 +func (q *QQwry) readData(num int, offset ...int64) (rs []byte) { + if len(offset) > 0 { + q.setOffset(offset[0]) + } + nums := int64(num) + end := q.Offset + nums + dataNum := int64(len(q.Data)) + if q.Offset > dataNum { + return nil + } + + if end > dataNum { + end = dataNum + } + rs = q.Data[q.Offset:end] + q.Offset = end + return +} + +// setOffset 设置偏移量 +func (q *QQwry) setOffset(offset int64) { + q.Offset = offset +} + +// Find ip地址查询对应归属地信息 +func (q *QQwry) Find(ip string) (res ResultQQwry) { + res = ResultQQwry{} + res.IP = ip + if strings.Count(ip, ".") != 3 { + return res + } + offset := q.searchIndex(binary.BigEndian.Uint32(net.ParseIP(ip).To4())) + if offset <= 0 { + return + } + + var area []byte + mode := q.readMode(offset + 4) + if mode == redirectMode1 { + countryOffset := q.readUInt24() + mode = q.readMode(countryOffset) + if mode == redirectMode2 { + c := q.readUInt24() + area = q.readString(c) + } else { + area = q.readString(countryOffset) + } + } else if mode == redirectMode2 { + countryOffset := q.readUInt24() + area = q.readString(countryOffset) + } else { + area = q.readString(offset + 4) + } + + enc := simplifiedchinese.GBK.NewDecoder() + res.Area, _ = enc.String(string(area)) + + return +} + +type ResultQQwry struct { + IP string `json:"ip"` + Area string `json:"area"` +} + +// readMode 获取偏移值类型 +func (q *QQwry) readMode(offset uint32) byte { + mode := q.readData(1, int64(offset)) + return mode[0] +} + +// readString 获取字符串 +func (q *QQwry) readString(offset uint32) []byte { + q.setOffset(int64(offset)) + data := make([]byte, 0, 30) + for { + buf := q.readData(1) + if buf[0] == 0 { + break + } + data = append(data, buf[0]) + } + return data +} + +// searchIndex 查找索引位置 +func (q *QQwry) searchIndex(ip uint32) uint32 { + header := q.readData(8, 0) + + start := binary.LittleEndian.Uint32(header[:4]) + end := binary.LittleEndian.Uint32(header[4:]) + + for { + mid := q.getMiddleOffset(start, end) + buf := q.readData(indexLen, int64(mid)) + _ip := binary.LittleEndian.Uint32(buf[:4]) + + if end-start == indexLen { + offset := byteToUInt32(buf[4:]) + buf = q.readData(indexLen) + if ip < binary.LittleEndian.Uint32(buf[:4]) { + return offset + } + return 0 + } + + if _ip > ip { + end = mid + } else if _ip < ip { + start = mid + } else if _ip == ip { + return byteToUInt32(buf[4:]) + } + } +} + +// readUInt24 +func (q *QQwry) readUInt24() uint32 { + buf := q.readData(3) + return byteToUInt32(buf) +} + +// getMiddleOffset +func (q *QQwry) getMiddleOffset(start uint32, end uint32) uint32 { + records := ((end - start) / indexLen) >> 1 + return start + records*indexLen +} + +// byteToUInt32 将 byte 转换为uint32 +func byteToUInt32(data []byte) uint32 { + i := uint32(data[0]) & 0xff + i |= (uint32(data[1]) << 8) & 0xff00 + i |= (uint32(data[2]) << 16) & 0xff0000 + return i +} diff --git a/agent/utils/redis/redis.go b/agent/utils/redis/redis.go new file mode 100644 index 000000000..703229df8 --- /dev/null +++ b/agent/utils/redis/redis.go @@ -0,0 +1,28 @@ +package redis + +import ( + "fmt" + + "github.com/1Panel-dev/1Panel/agent/global" + "github.com/go-redis/redis" +) + +type DBInfo struct { + Address string `json:"address"` + Port uint `json:"port"` + Password string `json:"password"` +} + +func NewRedisClient(conn DBInfo) (*redis.Client, error) { + client := redis.NewClient(&redis.Options{ + Addr: fmt.Sprintf("%s:%v", conn.Address, conn.Port), + Password: conn.Password, + DB: 0, + }) + + if _, err := client.Ping().Result(); err != nil { + global.LOG.Errorf("check redis conn failed, err: %v", err) + return client, err + } + return client, nil +} diff --git a/agent/utils/ssh/ssh.go b/agent/utils/ssh/ssh.go new file mode 100644 index 000000000..f487182d4 --- /dev/null +++ b/agent/utils/ssh/ssh.go @@ -0,0 +1,142 @@ +package ssh + +import ( + "bytes" + "fmt" + "io" + "strings" + "sync" + "time" + + gossh "golang.org/x/crypto/ssh" +) + +type ConnInfo struct { + User string `json:"user"` + Addr string `json:"addr"` + Port int `json:"port"` + AuthMode string `json:"authMode"` + Password string `json:"password"` + PrivateKey []byte `json:"privateKey"` + PassPhrase []byte `json:"passPhrase"` + DialTimeOut time.Duration `json:"dialTimeOut"` + + Client *gossh.Client `json:"client"` + Session *gossh.Session `json:"session"` + LastResult string `json:"lastResult"` +} + +func (c *ConnInfo) NewClient() (*ConnInfo, error) { + if strings.Contains(c.Addr, ":") { + c.Addr = fmt.Sprintf("[%s]", c.Addr) + } + config := &gossh.ClientConfig{} + config.SetDefaults() + addr := fmt.Sprintf("%s:%d", c.Addr, c.Port) + config.User = c.User + if c.AuthMode == "password" { + config.Auth = []gossh.AuthMethod{gossh.Password(c.Password)} + } else { + signer, err := makePrivateKeySigner(c.PrivateKey, c.PassPhrase) + if err != nil { + return nil, err + } + config.Auth = []gossh.AuthMethod{gossh.PublicKeys(signer)} + } + if c.DialTimeOut == 0 { + c.DialTimeOut = 5 * time.Second + } + config.Timeout = c.DialTimeOut + + config.HostKeyCallback = gossh.InsecureIgnoreHostKey() + proto := "tcp" + if strings.Contains(c.Addr, ":") { + proto = "tcp6" + } + client, err := gossh.Dial(proto, addr, config) + if nil != err { + return c, err + } + c.Client = client + return c, nil +} + +func (c *ConnInfo) Run(shell string) (string, error) { + if c.Client == nil { + if _, err := c.NewClient(); err != nil { + return "", err + } + } + session, err := c.Client.NewSession() + if err != nil { + return "", err + } + defer session.Close() + buf, err := session.CombinedOutput(shell) + + c.LastResult = string(buf) + return c.LastResult, err +} + +func (c *ConnInfo) Close() { + _ = c.Client.Close() +} + +type SshConn struct { + StdinPipe io.WriteCloser + ComboOutput *wsBufferWriter + Session *gossh.Session +} + +func (c *ConnInfo) NewSshConn(cols, rows int) (*SshConn, error) { + sshSession, err := c.Client.NewSession() + if err != nil { + return nil, err + } + + stdinP, err := sshSession.StdinPipe() + if err != nil { + return nil, err + } + + comboWriter := new(wsBufferWriter) + sshSession.Stdout = comboWriter + sshSession.Stderr = comboWriter + + modes := gossh.TerminalModes{ + gossh.ECHO: 1, + gossh.TTY_OP_ISPEED: 14400, + gossh.TTY_OP_OSPEED: 14400, + } + if err := sshSession.RequestPty("xterm", rows, cols, modes); err != nil { + return nil, err + } + if err := sshSession.Shell(); err != nil { + return nil, err + } + return &SshConn{StdinPipe: stdinP, ComboOutput: comboWriter, Session: sshSession}, nil +} + +func (s *SshConn) Close() { + if s.Session != nil { + s.Session.Close() + } +} + +type wsBufferWriter struct { + buffer bytes.Buffer + mu sync.Mutex +} + +func (w *wsBufferWriter) Write(p []byte) (int, error) { + w.mu.Lock() + defer w.mu.Unlock() + return w.buffer.Write(p) +} + +func makePrivateKeySigner(privateKey []byte, passPhrase []byte) (gossh.Signer, error) { + if len(passPhrase) != 0 { + return gossh.ParsePrivateKeyWithPassphrase(privateKey, passPhrase) + } + return gossh.ParsePrivateKey(privateKey) +} diff --git a/agent/utils/ssl/acme.go b/agent/utils/ssl/acme.go new file mode 100644 index 000000000..75efc9f49 --- /dev/null +++ b/agent/utils/ssl/acme.go @@ -0,0 +1,206 @@ +package ssl + +import ( + "crypto" + "crypto/ecdsa" + "crypto/rsa" + "crypto/x509" + "encoding/json" + "encoding/pem" + "fmt" + "io" + "net/http" + "net/url" + + "github.com/1Panel-dev/1Panel/agent/app/model" + "github.com/go-acme/lego/v4/certcrypto" + "github.com/go-acme/lego/v4/lego" + "github.com/go-acme/lego/v4/registration" +) + +type domainError struct { + Domain string + Error error +} + +type zeroSSLRes struct { + Success bool `json:"success"` + EabKid string `json:"eab_kid"` + EabHmacKey string `json:"eab_hmac_key"` +} + +type KeyType = certcrypto.KeyType + +const ( + KeyEC256 = certcrypto.EC256 + KeyEC384 = certcrypto.EC384 + KeyRSA2048 = certcrypto.RSA2048 + KeyRSA3072 = certcrypto.RSA3072 + KeyRSA4096 = certcrypto.RSA4096 +) + +func GetPrivateKey(priKey crypto.PrivateKey, keyType KeyType) ([]byte, error) { + var ( + marshal []byte + block *pem.Block + err error + ) + + switch keyType { + case KeyEC256, KeyEC384: + key := priKey.(*ecdsa.PrivateKey) + marshal, err = x509.MarshalECPrivateKey(key) + if err != nil { + return nil, err + } + block = &pem.Block{ + Type: "EC PRIVATE KEY", + Bytes: marshal, + } + case KeyRSA2048, KeyRSA3072, KeyRSA4096: + key := priKey.(*rsa.PrivateKey) + marshal = x509.MarshalPKCS1PrivateKey(key) + block = &pem.Block{ + Type: "RSA PRIVATE KEY", + Bytes: marshal, + } + } + + return pem.EncodeToMemory(block), nil +} + +func NewRegisterClient(acmeAccount *model.WebsiteAcmeAccount) (*AcmeClient, error) { + var ( + priKey crypto.PrivateKey + err error + ) + + if acmeAccount.PrivateKey != "" { + switch KeyType(acmeAccount.KeyType) { + case KeyEC256, KeyEC384: + block, _ := pem.Decode([]byte(acmeAccount.PrivateKey)) + priKey, err = x509.ParseECPrivateKey(block.Bytes) + if err != nil { + return nil, err + } + case KeyRSA2048, KeyRSA3072, KeyRSA4096: + block, _ := pem.Decode([]byte(acmeAccount.PrivateKey)) + priKey, err = x509.ParsePKCS1PrivateKey(block.Bytes) + if err != nil { + return nil, err + } + } + + } else { + priKey, err = certcrypto.GeneratePrivateKey(KeyType(acmeAccount.KeyType)) + if err != nil { + return nil, err + } + } + + myUser := &AcmeUser{ + Email: acmeAccount.Email, + Key: priKey, + } + config := newConfig(myUser, acmeAccount.Type) + client, err := lego.NewClient(config) + if err != nil { + return nil, err + } + var reg *registration.Resource + if acmeAccount.Type == "zerossl" || acmeAccount.Type == "google" { + if acmeAccount.Type == "zerossl" { + var res *zeroSSLRes + res, err = getZeroSSLEabCredentials(acmeAccount.Email) + if err != nil { + return nil, err + } + if res.Success { + acmeAccount.EabKid = res.EabKid + acmeAccount.EabHmacKey = res.EabHmacKey + } else { + return nil, fmt.Errorf("get zero ssl eab credentials failed") + } + } + + eabOptions := registration.RegisterEABOptions{ + TermsOfServiceAgreed: true, + Kid: acmeAccount.EabKid, + HmacEncoded: acmeAccount.EabHmacKey, + } + reg, err = client.Registration.RegisterWithExternalAccountBinding(eabOptions) + if err != nil { + return nil, err + } + } else { + reg, err = client.Registration.Register(registration.RegisterOptions{TermsOfServiceAgreed: true}) + if err != nil { + return nil, err + } + } + myUser.Registration = reg + + acmeClient := &AcmeClient{ + User: myUser, + Client: client, + Config: config, + } + + return acmeClient, nil +} + +func newConfig(user *AcmeUser, accountType string) *lego.Config { + config := lego.NewConfig(user) + switch accountType { + case "letsencrypt": + config.CADirURL = "https://acme-v02.api.letsencrypt.org/directory" + case "zerossl": + config.CADirURL = "https://acme.zerossl.com/v2/DV90" + case "buypass": + config.CADirURL = "https://api.buypass.com/acme/directory" + case "google": + config.CADirURL = "https://dv.acme-v02.api.pki.goog/directory" + } + + config.UserAgent = "1Panel" + config.Certificate.KeyType = certcrypto.RSA2048 + return config +} + +func getZeroSSLEabCredentials(email string) (*zeroSSLRes, error) { + baseURL := "https://api.zerossl.com/acme/eab-credentials-email" + params := url.Values{} + params.Add("email", email) + requestURL := fmt.Sprintf("%s?%s", baseURL, params.Encode()) + + req, err := http.NewRequest("POST", requestURL, nil) + if err != nil { + return nil, err + } + + req.Header.Set("Content-Type", "application/json") + + client := &http.Client{} + resp, err := client.Do(req) + if err != nil { + return nil, err + } + defer resp.Body.Close() + + body, err := io.ReadAll(resp.Body) + if err != nil { + return nil, err + } + + if resp.StatusCode != http.StatusOK { + return nil, fmt.Errorf("server returned non-200 status: %d %s", resp.StatusCode, http.StatusText(resp.StatusCode)) + } + + var result zeroSSLRes + err = json.Unmarshal(body, &result) + if err != nil { + return nil, err + } + + return &result, nil +} diff --git a/agent/utils/ssl/client.go b/agent/utils/ssl/client.go new file mode 100644 index 000000000..4799684d3 --- /dev/null +++ b/agent/utils/ssl/client.go @@ -0,0 +1,302 @@ +package ssl + +import ( + "crypto" + "encoding/json" + "os" + "time" + + "github.com/1Panel-dev/1Panel/agent/app/model" + "github.com/go-acme/lego/v4/acme" + "github.com/go-acme/lego/v4/acme/api" + "github.com/go-acme/lego/v4/certificate" + "github.com/go-acme/lego/v4/challenge" + "github.com/go-acme/lego/v4/challenge/dns01" + "github.com/go-acme/lego/v4/lego" + "github.com/go-acme/lego/v4/providers/dns/alidns" + "github.com/go-acme/lego/v4/providers/dns/cloudflare" + "github.com/go-acme/lego/v4/providers/dns/dnspod" + "github.com/go-acme/lego/v4/providers/dns/godaddy" + "github.com/go-acme/lego/v4/providers/dns/namecheap" + "github.com/go-acme/lego/v4/providers/dns/namedotcom" + "github.com/go-acme/lego/v4/providers/dns/namesilo" + "github.com/go-acme/lego/v4/providers/dns/tencentcloud" + "github.com/go-acme/lego/v4/providers/http/webroot" + "github.com/go-acme/lego/v4/registration" + "github.com/pkg/errors" +) + +type AcmeUser struct { + Email string + Registration *registration.Resource + Key crypto.PrivateKey +} + +func (u *AcmeUser) GetEmail() string { + return u.Email +} + +func (u *AcmeUser) GetRegistration() *registration.Resource { + return u.Registration +} +func (u *AcmeUser) GetPrivateKey() crypto.PrivateKey { + return u.Key +} + +type AcmeClient struct { + Config *lego.Config + Client *lego.Client + User *AcmeUser +} + +func NewAcmeClient(acmeAccount *model.WebsiteAcmeAccount) (*AcmeClient, error) { + if acmeAccount.Email == "" { + return nil, errors.New("email can not blank") + } + client, err := NewRegisterClient(acmeAccount) + if err != nil { + return nil, err + } + return client, nil +} + +type DnsType string + +const ( + DnsPod DnsType = "DnsPod" + AliYun DnsType = "AliYun" + CloudFlare DnsType = "CloudFlare" + NameSilo DnsType = "NameSilo" + NameCheap DnsType = "NameCheap" + NameCom DnsType = "NameCom" + Godaddy DnsType = "Godaddy" + TencentCloud DnsType = "TencentCloud" +) + +type DNSParam struct { + ID string `json:"id"` + Token string `json:"token"` + AccessKey string `json:"accessKey"` + SecretKey string `json:"secretKey"` + Email string `json:"email"` + APIkey string `json:"apiKey"` + APIUser string `json:"apiUser"` + APISecret string `json:"apiSecret"` + SecretID string `json:"secretID"` +} + +var ( + propagationTimeout = 30 * time.Minute + pollingInterval = 10 * time.Second + ttl = 3600 +) + +func (c *AcmeClient) UseDns(dnsType DnsType, params string, websiteSSL model.WebsiteSSL) error { + var ( + param DNSParam + p challenge.Provider + err error + ) + + if err = json.Unmarshal([]byte(params), ¶m); err != nil { + return err + } + + switch dnsType { + case DnsPod: + dnsPodConfig := dnspod.NewDefaultConfig() + dnsPodConfig.LoginToken = param.ID + "," + param.Token + dnsPodConfig.PropagationTimeout = propagationTimeout + dnsPodConfig.PollingInterval = pollingInterval + dnsPodConfig.TTL = ttl + p, err = dnspod.NewDNSProviderConfig(dnsPodConfig) + case AliYun: + alidnsConfig := alidns.NewDefaultConfig() + alidnsConfig.SecretKey = param.SecretKey + alidnsConfig.APIKey = param.AccessKey + alidnsConfig.PropagationTimeout = propagationTimeout + alidnsConfig.PollingInterval = pollingInterval + alidnsConfig.TTL = ttl + p, err = alidns.NewDNSProviderConfig(alidnsConfig) + case CloudFlare: + cloudflareConfig := cloudflare.NewDefaultConfig() + cloudflareConfig.AuthEmail = param.Email + cloudflareConfig.AuthToken = param.APIkey + cloudflareConfig.PropagationTimeout = propagationTimeout + cloudflareConfig.PollingInterval = pollingInterval + cloudflareConfig.TTL = 3600 + p, err = cloudflare.NewDNSProviderConfig(cloudflareConfig) + case NameCheap: + namecheapConfig := namecheap.NewDefaultConfig() + namecheapConfig.APIKey = param.APIkey + namecheapConfig.APIUser = param.APIUser + namecheapConfig.PropagationTimeout = propagationTimeout + namecheapConfig.PollingInterval = pollingInterval + namecheapConfig.TTL = ttl + p, err = namecheap.NewDNSProviderConfig(namecheapConfig) + case NameSilo: + nameSiloConfig := namesilo.NewDefaultConfig() + nameSiloConfig.APIKey = param.APIkey + nameSiloConfig.PropagationTimeout = propagationTimeout + nameSiloConfig.PollingInterval = pollingInterval + nameSiloConfig.TTL = ttl + p, err = namesilo.NewDNSProviderConfig(nameSiloConfig) + case Godaddy: + godaddyConfig := godaddy.NewDefaultConfig() + godaddyConfig.APIKey = param.APIkey + godaddyConfig.APISecret = param.APISecret + godaddyConfig.PropagationTimeout = propagationTimeout + godaddyConfig.PollingInterval = pollingInterval + godaddyConfig.TTL = ttl + p, err = godaddy.NewDNSProviderConfig(godaddyConfig) + case NameCom: + nameComConfig := namedotcom.NewDefaultConfig() + nameComConfig.APIToken = param.Token + nameComConfig.Username = param.APIUser + nameComConfig.PropagationTimeout = propagationTimeout + nameComConfig.PollingInterval = pollingInterval + nameComConfig.TTL = ttl + p, err = namedotcom.NewDNSProviderConfig(nameComConfig) + case TencentCloud: + tencentCloudConfig := tencentcloud.NewDefaultConfig() + tencentCloudConfig.SecretID = param.SecretID + tencentCloudConfig.SecretKey = param.SecretKey + tencentCloudConfig.PropagationTimeout = propagationTimeout + tencentCloudConfig.PollingInterval = pollingInterval + tencentCloudConfig.TTL = ttl + p, err = tencentcloud.NewDNSProviderConfig(tencentCloudConfig) + } + if err != nil { + return err + } + var nameservers []string + if websiteSSL.Nameserver1 != "" { + nameservers = append(nameservers, websiteSSL.Nameserver1) + } + if websiteSSL.Nameserver2 != "" { + nameservers = append(nameservers, websiteSSL.Nameserver2) + } + if websiteSSL.DisableCNAME { + _ = os.Setenv("LEGO_DISABLE_CNAME_SUPPORT", "true") + } else { + _ = os.Setenv("LEGO_DISABLE_CNAME_SUPPORT", "false") + } + + return c.Client.Challenge.SetDNS01Provider(p, + dns01.CondOption(len(nameservers) > 0, + dns01.AddRecursiveNameservers(nameservers)), + dns01.CondOption(websiteSSL.SkipDNS, + dns01.DisableCompletePropagationRequirement()), + dns01.AddDNSTimeout(10*time.Minute), + ) +} + +func (c *AcmeClient) UseManualDns() error { + p := &manualDnsProvider{} + if err := c.Client.Challenge.SetDNS01Provider(p, dns01.AddDNSTimeout(10*time.Minute)); err != nil { + return err + } + return nil +} + +func (c *AcmeClient) UseHTTP(path string) error { + httpProvider, err := webroot.NewHTTPProvider(path) + if err != nil { + return err + } + + err = c.Client.Challenge.SetHTTP01Provider(httpProvider) + if err != nil { + return err + } + return nil +} + +func (c *AcmeClient) ObtainSSL(domains []string, privateKey crypto.PrivateKey) (certificate.Resource, error) { + request := certificate.ObtainRequest{ + Domains: domains, + Bundle: true, + PrivateKey: privateKey, + } + + certificates, err := c.Client.Certificate.Obtain(request) + if err != nil { + return certificate.Resource{}, err + } + + return *certificates, nil +} + +type Resolve struct { + Key string + Value string + Err string +} + +type manualDnsProvider struct { + Resolve *Resolve +} + +func (p *manualDnsProvider) Present(domain, token, keyAuth string) error { + return nil +} + +func (p *manualDnsProvider) CleanUp(domain, token, keyAuth string) error { + return nil +} + +func (c *AcmeClient) GetDNSResolve(domains []string) (map[string]Resolve, error) { + core, err := api.New(c.Config.HTTPClient, c.Config.UserAgent, c.Config.CADirURL, c.User.Registration.URI, c.User.Key) + if err != nil { + return nil, err + } + order, err := core.Orders.New(domains) + if err != nil { + return nil, err + } + resolves := make(map[string]Resolve) + resc, errc := make(chan acme.Authorization), make(chan domainError) + for _, authzURL := range order.Authorizations { + go func(authzURL string) { + authz, err := core.Authorizations.Get(authzURL) + if err != nil { + errc <- domainError{Domain: authz.Identifier.Value, Error: err} + return + } + resc <- authz + }(authzURL) + } + + var responses []acme.Authorization + for i := 0; i < len(order.Authorizations); i++ { + select { + case res := <-resc: + responses = append(responses, res) + case err := <-errc: + resolves[err.Domain] = Resolve{Err: err.Error.Error()} + } + } + close(resc) + close(errc) + + for _, auth := range responses { + domain := challenge.GetTargetedDomain(auth) + chlng, err := challenge.FindChallenge(challenge.DNS01, auth) + if err != nil { + resolves[domain] = Resolve{Err: err.Error()} + continue + } + keyAuth, err := core.GetKeyAuthorization(chlng.Token) + if err != nil { + resolves[domain] = Resolve{Err: err.Error()} + continue + } + challengeInfo := dns01.GetChallengeInfo(domain, keyAuth) + resolves[domain] = Resolve{ + Key: challengeInfo.FQDN, + Value: challengeInfo.Value, + } + } + + return resolves, nil +} diff --git a/agent/utils/systemctl/systemctl.go b/agent/utils/systemctl/systemctl.go new file mode 100644 index 000000000..0f7ef19db --- /dev/null +++ b/agent/utils/systemctl/systemctl.go @@ -0,0 +1,64 @@ +package systemctl + +import ( + "fmt" + "github.com/pkg/errors" + "os/exec" + "strings" +) + +func RunSystemCtl(args ...string) (string, error) { + cmd := exec.Command("systemctl", args...) + output, err := cmd.CombinedOutput() + if err != nil { + return string(output), fmt.Errorf("failed to run command: %w", err) + } + return string(output), nil +} + +func IsActive(serviceName string) (bool, error) { + out, err := RunSystemCtl("is-active", serviceName) + if err != nil { + return false, err + } + return out == "active\n", nil +} + +func IsEnable(serviceName string) (bool, error) { + out, err := RunSystemCtl("is-enabled", serviceName) + if err != nil { + return false, err + } + return out == "enabled\n", nil +} + +func IsExist(serviceName string) (bool, error) { + out, err := RunSystemCtl("is-enabled", serviceName) + if err != nil { + if strings.Contains(out, "disabled") { + return true, nil + } + return false, nil + } + return true, nil +} + +func handlerErr(out string, err error) error { + if err != nil { + if out != "" { + return errors.New(out) + } + return err + } + return nil +} + +func Restart(serviceName string) error { + out, err := RunSystemCtl("restart", serviceName) + return handlerErr(out, err) +} + +func Operate(operate, serviceName string) error { + out, err := RunSystemCtl(operate, serviceName) + return handlerErr(out, err) +} diff --git a/agent/utils/terminal/local_cmd.go b/agent/utils/terminal/local_cmd.go new file mode 100644 index 000000000..e1beb36bb --- /dev/null +++ b/agent/utils/terminal/local_cmd.go @@ -0,0 +1,93 @@ +package terminal + +import ( + "os" + "os/exec" + "syscall" + "time" + "unsafe" + + "github.com/1Panel-dev/1Panel/agent/global" + "github.com/creack/pty" + "github.com/pkg/errors" +) + +const ( + DefaultCloseSignal = syscall.SIGINT + DefaultCloseTimeout = 10 * time.Second +) + +type LocalCommand struct { + closeSignal syscall.Signal + closeTimeout time.Duration + + cmd *exec.Cmd + pty *os.File +} + +func NewCommand(commands []string) (*LocalCommand, error) { + cmd := exec.Command("docker", commands...) + + pty, err := pty.Start(cmd) + if err != nil { + return nil, errors.Wrapf(err, "failed to start command") + } + + lcmd := &LocalCommand{ + closeSignal: DefaultCloseSignal, + closeTimeout: DefaultCloseTimeout, + + cmd: cmd, + pty: pty, + } + + return lcmd, nil +} + +func (lcmd *LocalCommand) Read(p []byte) (n int, err error) { + return lcmd.pty.Read(p) +} + +func (lcmd *LocalCommand) Write(p []byte) (n int, err error) { + return lcmd.pty.Write(p) +} + +func (lcmd *LocalCommand) Close() error { + if lcmd.cmd != nil && lcmd.cmd.Process != nil { + _ = lcmd.cmd.Process.Kill() + } + _ = lcmd.pty.Close() + return nil +} + +func (lcmd *LocalCommand) ResizeTerminal(width int, height int) error { + window := struct { + row uint16 + col uint16 + x uint16 + y uint16 + }{ + uint16(height), + uint16(width), + 0, + 0, + } + _, _, errno := syscall.Syscall( + syscall.SYS_IOCTL, + lcmd.pty.Fd(), + syscall.TIOCSWINSZ, + uintptr(unsafe.Pointer(&window)), + ) + if errno != 0 { + return errno + } else { + return nil + } +} + +func (lcmd *LocalCommand) Wait(quitChan chan bool) { + if err := lcmd.cmd.Wait(); err != nil { + global.LOG.Errorf("ssh session wait failed, err: %v", err) + setQuit(quitChan) + } +} diff --git a/agent/utils/terminal/ws_local_session.go b/agent/utils/terminal/ws_local_session.go new file mode 100644 index 000000000..b59024ef1 --- /dev/null +++ b/agent/utils/terminal/ws_local_session.go @@ -0,0 +1,122 @@ +package terminal + +import ( + "encoding/base64" + "encoding/json" + "sync" + + "github.com/1Panel-dev/1Panel/agent/global" + "github.com/gorilla/websocket" + "github.com/pkg/errors" +) + +type LocalWsSession struct { + slave *LocalCommand + wsConn *websocket.Conn + + allowCtrlC bool + writeMutex sync.Mutex +} + +func NewLocalWsSession(cols, rows int, wsConn *websocket.Conn, slave *LocalCommand, allowCtrlC bool) (*LocalWsSession, error) { + if err := slave.ResizeTerminal(cols, rows); err != nil { + global.LOG.Errorf("ssh pty change windows size failed, err: %v", err) + } + + return &LocalWsSession{ + slave: slave, + wsConn: wsConn, + + allowCtrlC: allowCtrlC, + }, nil +} + +func (sws *LocalWsSession) Start(quitChan chan bool) { + go sws.handleSlaveEvent(quitChan) + go sws.receiveWsMsg(quitChan) +} + +func (sws *LocalWsSession) handleSlaveEvent(exitCh chan bool) { + defer setQuit(exitCh) + defer global.LOG.Debug("thread of handle slave event has exited now") + + buffer := make([]byte, 1024) + for { + select { + case <-exitCh: + return + default: + n, _ := sws.slave.Read(buffer) + _ = sws.masterWrite(buffer[:n]) + } + } +} + +func (sws *LocalWsSession) masterWrite(data []byte) error { + sws.writeMutex.Lock() + defer sws.writeMutex.Unlock() + wsData, err := json.Marshal(WsMsg{ + Type: WsMsgCmd, + Data: base64.StdEncoding.EncodeToString(data), + }) + if err != nil { + return errors.Wrapf(err, "failed to encoding to json") + } + err = sws.wsConn.WriteMessage(websocket.TextMessage, wsData) + if err != nil { + return errors.Wrapf(err, "failed to write to master") + } + return nil +} + +func (sws *LocalWsSession) receiveWsMsg(exitCh chan bool) { + defer func() { + if r := recover(); r != nil { + global.LOG.Errorf("A panic occurred during receive ws message, error message: %v", r) + } + }() + wsConn := sws.wsConn + defer setQuit(exitCh) + defer global.LOG.Debug("thread of receive ws msg has exited now") + for { + select { + case <-exitCh: + return + default: + _, wsData, err := wsConn.ReadMessage() + if err != nil { + global.LOG.Errorf("reading webSocket message failed, err: %v", err) + return + } + msgObj := WsMsg{} + _ = json.Unmarshal(wsData, &msgObj) + switch msgObj.Type { + case WsMsgResize: + if msgObj.Cols > 0 && msgObj.Rows > 0 { + if err := sws.slave.ResizeTerminal(msgObj.Cols, msgObj.Rows); err != nil { + global.LOG.Errorf("ssh pty change windows size failed, err: %v", err) + } + } + case WsMsgCmd: + decodeBytes, err := base64.StdEncoding.DecodeString(msgObj.Data) + if err != nil { + global.LOG.Errorf("websock cmd string base64 decoding failed, err: %v", err) + } + if string(decodeBytes) != "\x03" || sws.allowCtrlC { + sws.sendWebsocketInputCommandToSshSessionStdinPipe(decodeBytes) + } + case WsMsgHeartbeat: + err = wsConn.WriteMessage(websocket.TextMessage, wsData) + if err != nil { + global.LOG.Errorf("ssh sending heartbeat to webSocket failed, err: %v", err) + } + } + } + } +} + +func (sws *LocalWsSession) sendWebsocketInputCommandToSshSessionStdinPipe(cmdBytes []byte) { + if _, err := sws.slave.Write(cmdBytes); err != nil { + global.LOG.Errorf("ws cmd bytes write to ssh.stdin pipe failed, err: %v", err) + } +} diff --git a/agent/utils/terminal/ws_session.go b/agent/utils/terminal/ws_session.go new file mode 100644 index 000000000..54411f967 --- /dev/null +++ b/agent/utils/terminal/ws_session.go @@ -0,0 +1,218 @@ +package terminal + +import ( + "bytes" + "encoding/base64" + "encoding/json" + "io" + "sync" + "time" + + "github.com/1Panel-dev/1Panel/agent/global" + "github.com/gorilla/websocket" + "golang.org/x/crypto/ssh" +) + +type safeBuffer struct { + buffer bytes.Buffer + mu sync.Mutex +} + +func (w *safeBuffer) Write(p []byte) (int, error) { + w.mu.Lock() + defer w.mu.Unlock() + return w.buffer.Write(p) +} +func (w *safeBuffer) Bytes() []byte { + w.mu.Lock() + defer w.mu.Unlock() + return w.buffer.Bytes() +} +func (w *safeBuffer) Reset() { + w.mu.Lock() + defer w.mu.Unlock() + w.buffer.Reset() +} + +const ( + WsMsgCmd = "cmd" + WsMsgResize = "resize" + WsMsgHeartbeat = "heartbeat" +) + +type WsMsg struct { + Type string `json:"type"` + Data string `json:"data,omitempty"` // WsMsgCmd + Cols int `json:"cols,omitempty"` // WsMsgResize + Rows int `json:"rows,omitempty"` // WsMsgResize + Timestamp int `json:"timestamp,omitempty"` // WsMsgHeartbeat +} + +type LogicSshWsSession struct { + stdinPipe io.WriteCloser + comboOutput *safeBuffer + logBuff *safeBuffer + inputFilterBuff *safeBuffer + session *ssh.Session + wsConn *websocket.Conn + isAdmin bool + IsFlagged bool +} + +func NewLogicSshWsSession(cols, rows int, isAdmin bool, sshClient *ssh.Client, wsConn *websocket.Conn) (*LogicSshWsSession, error) { + sshSession, err := sshClient.NewSession() + if err != nil { + return nil, err + } + + stdinP, err := sshSession.StdinPipe() + if err != nil { + return nil, err + } + + comboWriter := new(safeBuffer) + logBuf := new(safeBuffer) + inputBuf := new(safeBuffer) + sshSession.Stdout = comboWriter + sshSession.Stderr = comboWriter + + modes := ssh.TerminalModes{ + ssh.ECHO: 1, + ssh.TTY_OP_ISPEED: 14400, + ssh.TTY_OP_OSPEED: 14400, + } + if err := sshSession.RequestPty("xterm", rows, cols, modes); err != nil { + return nil, err + } + if err := sshSession.Shell(); err != nil { + return nil, err + } + return &LogicSshWsSession{ + stdinPipe: stdinP, + comboOutput: comboWriter, + logBuff: logBuf, + inputFilterBuff: inputBuf, + session: sshSession, + wsConn: wsConn, + isAdmin: isAdmin, + IsFlagged: false, + }, nil +} + +func (sws *LogicSshWsSession) Close() { + if sws.session != nil { + sws.session.Close() + } + if sws.logBuff != nil { + sws.logBuff = nil + } + if sws.comboOutput != nil { + sws.comboOutput = nil + } +} +func (sws *LogicSshWsSession) Start(quitChan chan bool) { + go sws.receiveWsMsg(quitChan) + go sws.sendComboOutput(quitChan) +} + +func (sws *LogicSshWsSession) receiveWsMsg(exitCh chan bool) { + defer func() { + if r := recover(); r != nil { + global.LOG.Errorf("[A panic occurred during receive ws message, error message: %v", r) + } + }() + wsConn := sws.wsConn + defer setQuit(exitCh) + for { + select { + case <-exitCh: + return + default: + _, wsData, err := wsConn.ReadMessage() + if err != nil { + return + } + msgObj := WsMsg{} + _ = json.Unmarshal(wsData, &msgObj) + switch msgObj.Type { + case WsMsgResize: + if msgObj.Cols > 0 && msgObj.Rows > 0 { + if err := sws.session.WindowChange(msgObj.Rows, msgObj.Cols); err != nil { + global.LOG.Errorf("ssh pty change windows size failed, err: %v", err) + } + } + case WsMsgCmd: + decodeBytes, err := base64.StdEncoding.DecodeString(msgObj.Data) + if err != nil { + global.LOG.Errorf("websock cmd string base64 decoding failed, err: %v", err) + } + sws.sendWebsocketInputCommandToSshSessionStdinPipe(decodeBytes) + case WsMsgHeartbeat: + // 接收到心跳包后将心跳包原样返回,可以用于网络延迟检测等情况 + err = wsConn.WriteMessage(websocket.TextMessage, wsData) + if err != nil { + global.LOG.Errorf("ssh sending heartbeat to webSocket failed, err: %v", err) + } + } + } + } +} + +func (sws *LogicSshWsSession) sendWebsocketInputCommandToSshSessionStdinPipe(cmdBytes []byte) { + if _, err := sws.stdinPipe.Write(cmdBytes); err != nil { + global.LOG.Errorf("ws cmd bytes write to ssh.stdin pipe failed, err: %v", err) + } +} + +func (sws *LogicSshWsSession) sendComboOutput(exitCh chan bool) { + wsConn := sws.wsConn + defer setQuit(exitCh) + + tick := time.NewTicker(time.Millisecond * time.Duration(60)) + defer tick.Stop() + for { + select { + case <-tick.C: + if sws.comboOutput == nil { + return + } + bs := sws.comboOutput.Bytes() + if len(bs) > 0 { + wsData, err := json.Marshal(WsMsg{ + Type: WsMsgCmd, + Data: base64.StdEncoding.EncodeToString(bs), + }) + if err != nil { + global.LOG.Errorf("encoding combo output to json failed, err: %v", err) + continue + } + err = wsConn.WriteMessage(websocket.TextMessage, wsData) + if err != nil { + global.LOG.Errorf("ssh sending combo output to webSocket failed, err: %v", err) + } + _, err = sws.logBuff.Write(bs) + if err != nil { + global.LOG.Errorf("combo output to log buffer failed, err: %v", err) + } + sws.comboOutput.buffer.Reset() + } + if string(bs) == string([]byte{13, 10, 108, 111, 103, 111, 117, 116, 13, 10}) { + sws.Close() + return + } + + case <-exitCh: + return + } + } +} + +func (sws *LogicSshWsSession) Wait(quitChan chan bool) { + if err := sws.session.Wait(); err != nil { + setQuit(quitChan) + } +} + +func setQuit(ch chan bool) { + ch <- true +} diff --git a/agent/utils/toolbox/fail2ban.go b/agent/utils/toolbox/fail2ban.go new file mode 100644 index 000000000..0026cd5be --- /dev/null +++ b/agent/utils/toolbox/fail2ban.go @@ -0,0 +1,181 @@ +package toolbox + +import ( + "fmt" + "os" + "strings" + + "github.com/1Panel-dev/1Panel/agent/global" + "github.com/1Panel-dev/1Panel/agent/utils/cmd" + "github.com/1Panel-dev/1Panel/agent/utils/systemctl" +) + +type Fail2ban struct{} + +const defaultPath = "/etc/fail2ban/jail.local" + +type FirewallClient interface { + Status() (bool, bool, bool) + Version() (string, error) + Operate(operate string) error + OperateSSHD(operate, ip string) error +} + +func NewFail2Ban() (*Fail2ban, error) { + isExist, _ := systemctl.IsExist("fail2ban.service") + if isExist { + if _, err := os.Stat(defaultPath); err != nil { + if err := initLocalFile(); err != nil { + return nil, err + } + stdout, err := cmd.Exec("systemctl restart fail2ban.service") + if err != nil { + global.LOG.Errorf("restart fail2ban failed, err: %s", stdout) + return nil, err + } + } + } + return &Fail2ban{}, nil +} + +func (f *Fail2ban) Status() (bool, bool, bool) { + isEnable, _ := systemctl.IsEnable("fail2ban.service") + isActive, _ := systemctl.IsActive("fail2ban.service") + isExist, _ := systemctl.IsExist("fail2ban.service") + + return isEnable, isActive, isExist +} + +func (f *Fail2ban) Version() string { + stdout, err := cmd.Exec("fail2ban-client version") + if err != nil { + global.LOG.Errorf("load the fail2ban version failed, err: %s", stdout) + return "-" + } + return strings.ReplaceAll(stdout, "\n", "") +} + +func (f *Fail2ban) Operate(operate string) error { + switch operate { + case "start", "restart", "stop", "enable", "disable": + stdout, err := cmd.Execf("systemctl %s fail2ban.service", operate) + if err != nil { + return fmt.Errorf("%s the fail2ban.service failed, err: %s", operate, stdout) + } + return nil + case "reload": + stdout, err := cmd.Exec("fail2ban-client reload") + if err != nil { + return fmt.Errorf("fail2ban-client reload, err: %s", stdout) + } + return nil + default: + return fmt.Errorf("not support such operation: %v", operate) + } +} + +func (f *Fail2ban) ReBanIPs(ips []string) error { + ipItems, _ := f.ListBanned() + stdout, err := cmd.Execf("fail2ban-client unban --all") + if err != nil { + stdout1, err := cmd.Execf("fail2ban-client set sshd banip %s", strings.Join(ipItems, " ")) + if err != nil { + global.LOG.Errorf("rebanip after fail2ban-client unban --all failed, err: %s", stdout1) + } + return fmt.Errorf("fail2ban-client unban --all failed, err: %s", stdout) + } + stdout1, err := cmd.Execf("fail2ban-client set sshd banip %s", strings.Join(ips, " ")) + if err != nil { + return fmt.Errorf("handle `fail2ban-client set sshd banip %s` failed, err: %s", strings.Join(ips, " "), stdout1) + } + return nil +} + +func (f *Fail2ban) ListBanned() ([]string, error) { + var lists []string + stdout, err := cmd.Exec("fail2ban-client status sshd | grep 'Banned IP list:'") + if err != nil { + return lists, err + } + itemList := strings.Split(strings.Trim(stdout, "\n"), "Banned IP list:") + if len(itemList) != 2 { + return lists, nil + } + + ips := strings.Fields(itemList[1]) + for _, item := range ips { + if len(item) != 0 { + lists = append(lists, item) + } + } + return lists, nil +} + +func (f *Fail2ban) ListIgnore() ([]string, error) { + var lists []string + stdout, err := cmd.Exec("fail2ban-client get sshd ignoreip") + if err != nil { + return lists, err + } + stdout = strings.ReplaceAll(stdout, "|", "") + stdout = strings.ReplaceAll(stdout, "`", "") + stdout = strings.ReplaceAll(stdout, "\n", "") + addrs := strings.Split(stdout, "-") + for _, addr := range addrs { + if !strings.HasPrefix(addr, " ") { + continue + } + lists = append(lists, strings.ReplaceAll(addr, " ", "")) + } + return lists, nil +} + +func initLocalFile() error { + f, err := os.Create(defaultPath) + if err != nil { + return err + } + defer f.Close() + initFile := `#DEFAULT-START +[DEFAULT] +bantime = 600 +findtime = 300 +maxretry = 5 +banaction = $banaction +action = %(action_mwl)s +#DEFAULT-END + +[sshd] +ignoreip = 127.0.0.1/8 +enabled = true +filter = sshd +port = 22 +maxretry = 5 +findtime = 300 +bantime = 600 +banaction = $banaction +action = %(action_mwl)s +logpath = $logpath` + + banaction := "" + if active, _ := systemctl.IsActive("firewalld"); active { + banaction = "firewallcmd-ipset" + } else if active, _ := systemctl.IsActive("ufw"); active { + banaction = "ufw" + } else { + banaction = "iptables-allports" + } + initFile = strings.ReplaceAll(initFile, "$banaction", banaction) + + logPath := "" + if _, err := os.Stat("/var/log/secure"); err == nil { + logPath = "/var/log/secure" + } else { + logPath = "/var/log/auth.log" + } + initFile = strings.ReplaceAll(initFile, "$logpath", logPath) + if err := os.WriteFile(defaultPath, []byte(initFile), 0640); err != nil { + return err + } + return nil +} diff --git a/agent/utils/toolbox/pure-ftpd.go b/agent/utils/toolbox/pure-ftpd.go new file mode 100644 index 000000000..6936a518c --- /dev/null +++ b/agent/utils/toolbox/pure-ftpd.go @@ -0,0 +1,272 @@ +package toolbox + +import ( + "errors" + "fmt" + "os" + "os/user" + "path" + "path/filepath" + "strings" + "time" + + "github.com/1Panel-dev/1Panel/agent/constant" + "github.com/1Panel-dev/1Panel/agent/global" + "github.com/1Panel-dev/1Panel/agent/utils/cmd" + "github.com/1Panel-dev/1Panel/agent/utils/systemctl" +) + +type Ftp struct { + DefaultUser string + DefaultGroup string +} + +type FtpClient interface { + Status() (bool, bool) + Operate(operate string) error + LoadList() ([]FtpList, error) + UserAdd(username, path, passwd string) error + UserDel(username string) error + SetPasswd(username, passwd string) error + Reload() error + LoadLogs() ([]FtpLog, error) +} + +func NewFtpClient() (*Ftp, error) { + userItem, err := user.LookupId("1000") + if err == nil { + groupItem, err := user.LookupGroupId(userItem.Gid) + if err != nil { + return nil, err + } + return &Ftp{DefaultUser: userItem.Username, DefaultGroup: groupItem.Name}, err + } + if err.Error() != user.UnknownUserIdError(1000).Error() { + return nil, err + } + + groupItem, err := user.LookupGroupId("1000") + if err == nil { + stdout2, err := cmd.Execf("useradd -u 1000 -g %s %s", groupItem.Name, "1panel") + if err != nil { + return nil, errors.New(stdout2) + } + return &Ftp{DefaultUser: "1panel", DefaultGroup: groupItem.Name}, nil + } + if err.Error() != user.UnknownGroupIdError("1000").Error() { + return nil, err + } + stdout, err := cmd.Exec("groupadd -g 1000 1panel") + if err != nil { + return nil, errors.New(string(stdout)) + } + stdout2, err := cmd.Exec("useradd -u 1000 -g 1panel 1panel") + if err != nil { + return nil, errors.New(stdout2) + } + return &Ftp{DefaultUser: "1panel", DefaultGroup: "1panel"}, nil +} + +func (f *Ftp) Status() (bool, bool) { + isActive, _ := systemctl.IsActive("pure-ftpd.service") + isExist, _ := systemctl.IsExist("pure-ftpd.service") + + return isActive, isExist +} + +func (f *Ftp) Operate(operate string) error { + switch operate { + case "start", "restart", "stop": + stdout, err := cmd.Execf("systemctl %s pure-ftpd.service", operate) + if err != nil { + return fmt.Errorf("%s the pure-ftpd.service failed, err: %s", operate, stdout) + } + return nil + default: + return fmt.Errorf("not support such operation: %v", operate) + } +} + +func (f *Ftp) UserAdd(username, passwd, path string) error { + std, err := cmd.Execf("pure-pw useradd %s -u %s -d %s < 0 && processConfig.Pid != proc.Pid { + return + } + if procName, err := proc.Name(); err == nil { + procData.Name = procName + } else { + procData.Name = "" + } + if processConfig.Name != "" && !strings.Contains(procData.Name, processConfig.Name) { + return + } + if username, err := proc.Username(); err == nil { + procData.Username = username + } + if processConfig.Username != "" && !strings.Contains(procData.Username, processConfig.Username) { + return + } + procData.PPID, _ = proc.Ppid() + statusArray, _ := proc.Status() + if len(statusArray) > 0 { + procData.Status = strings.Join(statusArray, ",") + } + createTime, procErr := proc.CreateTime() + if procErr == nil { + t := time.Unix(createTime/1000, 0) + procData.StartTime = t.Format("2006-1-2 15:04:05") + } + procData.NumThreads, _ = proc.NumThreads() + connections, procErr := proc.Connections() + if procErr == nil { + procData.NumConnections = len(connections) + for _, conn := range connections { + if conn.Laddr.IP != "" || conn.Raddr.IP != "" { + procData.Connects = append(procData.Connects, processConnect{ + Status: conn.Status, + Laddr: conn.Laddr, + Raddr: conn.Raddr, + }) + } + } + } + procData.CpuValue, _ = proc.CPUPercent() + procData.CpuPercent = fmt.Sprintf("%.2f", procData.CpuValue) + "%" + menInfo, procErr := proc.MemoryInfo() + if procErr == nil { + procData.Rss = formatBytes(menInfo.RSS) + procData.RssValue = menInfo.RSS + procData.Data = formatBytes(menInfo.Data) + procData.VMS = formatBytes(menInfo.VMS) + procData.HWM = formatBytes(menInfo.HWM) + procData.Stack = formatBytes(menInfo.Stack) + procData.Locked = formatBytes(menInfo.Locked) + procData.Swap = formatBytes(menInfo.Swap) + } else { + procData.Rss = "--" + procData.Data = "--" + procData.VMS = "--" + procData.HWM = "--" + procData.Stack = "--" + procData.Locked = "--" + procData.Swap = "--" + + procData.RssValue = 0 + } + ioStat, procErr := proc.IOCounters() + if procErr == nil { + procData.DiskWrite = formatBytes(ioStat.WriteBytes) + procData.DiskRead = formatBytes(ioStat.ReadBytes) + } else { + procData.DiskWrite = "--" + procData.DiskRead = "--" + } + procData.CmdLine, _ = proc.Cmdline() + procData.OpenFiles, _ = proc.OpenFiles() + procData.Envs, _ = proc.Environ() + + resultMutex.Lock() + result = append(result, procData) + resultMutex.Unlock() + } + + chunkSize := (len(processes) + numWorkers - 1) / numWorkers + for i := 0; i < numWorkers; i++ { + wg.Add(1) + start := i * chunkSize + end := (i + 1) * chunkSize + if end > len(processes) { + end = len(processes) + } + + go func(start, end int) { + defer wg.Done() + for j := start; j < end; j++ { + handleData(processes[j]) + } + }(start, end) + } + + wg.Wait() + + sort.Slice(result, func(i, j int) bool { + return result[i].PID < result[j].PID + }) + res, err = json.Marshal(result) + return +} + +func getSSHSessions(config SSHSessionConfig) (res []byte, err error) { + var ( + result []sshSession + users []host.UserStat + processes []*process.Process + ) + processes, err = process.Processes() + if err != nil { + return + } + users, err = host.Users() + if err != nil { + return + } + for _, proc := range processes { + name, _ := proc.Name() + if name != "sshd" || proc.Pid == 0 { + continue + } + connections, _ := proc.Connections() + for _, conn := range connections { + for _, user := range users { + if user.Host == "" { + continue + } + if conn.Raddr.IP == user.Host { + if config.LoginUser != "" && !strings.Contains(user.User, config.LoginUser) { + continue + } + if config.LoginIP != "" && !strings.Contains(user.Host, config.LoginIP) { + continue + } + if terminal, err := proc.Cmdline(); err == nil { + if strings.Contains(terminal, user.Terminal) { + session := sshSession{ + Username: user.User, + Host: user.Host, + Terminal: user.Terminal, + PID: proc.Pid, + } + t := time.Unix(int64(user.Started), 0) + session.LoginTime = t.Format("2006-1-2 15:04:05") + result = append(result, session) + } + } + } + } + } + } + res, err = json.Marshal(result) + return +} + +var netTypes = [...]string{"tcp", "udp"} + +func getNetConnections(config NetConfig) (res []byte, err error) { + var ( + result []processConnect + proc *process.Process + ) + for _, netType := range netTypes { + connections, _ := net.Connections(netType) + if err == nil { + for _, conn := range connections { + if config.ProcessID > 0 && config.ProcessID != conn.Pid { + continue + } + proc, err = process.NewProcess(conn.Pid) + if err == nil { + name, _ := proc.Name() + if name != "" && config.ProcessName != "" && !strings.Contains(name, config.ProcessName) { + continue + } + if config.Port > 0 && config.Port != conn.Laddr.Port && config.Port != conn.Raddr.Port { + continue + } + result = append(result, processConnect{ + Type: netType, + Status: conn.Status, + Laddr: conn.Laddr, + Raddr: conn.Raddr, + PID: conn.Pid, + Name: name, + }) + } + + } + } + } + res, err = json.Marshal(result) + return +} diff --git a/agent/utils/xpack/xpack.go b/agent/utils/xpack/xpack.go new file mode 100644 index 000000000..19ea1843b --- /dev/null +++ b/agent/utils/xpack/xpack.go @@ -0,0 +1,37 @@ +//go:build !xpack + +package xpack + +import ( + "crypto/tls" + "net" + "net/http" + "time" + + "github.com/1Panel-dev/1Panel/agent/app/model" + "github.com/1Panel-dev/1Panel/agent/buserr" + "github.com/1Panel-dev/1Panel/agent/constant" +) + +func RemoveTamper(website string) {} + +func LoadRequestTransport() *http.Transport { + return &http.Transport{ + TLSClientConfig: &tls.Config{InsecureSkipVerify: true}, + DialContext: (&net.Dialer{ + Timeout: 60 * time.Second, + KeepAlive: 60 * time.Second, + }).DialContext, + TLSHandshakeTimeout: 5 * time.Second, + ResponseHeaderTimeout: 10 * time.Second, + IdleConnTimeout: 15 * time.Second, + } +} + +func LoadGpuInfo() []interface{} { + return nil +} + +func StartClam(startClam model.Clam, isUpdate bool) (int, error) { + return 0, buserr.New(constant.ErrXpackNotFound) +} diff --git a/agent/utils/xpack/xpack_xpack.go b/agent/utils/xpack/xpack_xpack.go new file mode 120000 index 000000000..aec4042e5 --- /dev/null +++ b/agent/utils/xpack/xpack_xpack.go @@ -0,0 +1 @@ +/Users/slooop/Documents/mycode/xpack-backend/other/xpack_xpack.go \ No newline at end of file