diff --git a/backend/app/api/v1/website.go b/backend/app/api/v1/website.go index d8b7a1458..300a8f676 100644 --- a/backend/app/api/v1/website.go +++ b/backend/app/api/v1/website.go @@ -561,6 +561,28 @@ func (b *BaseApi) UpdatePHPFile(c *gin.Context) { 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":[],"BeforeFuntions":[{"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 := c.ShouldBindJSON(&req); err != nil { + helper.ErrorWithDetail(c, constant.CodeErrBadRequest, constant.ErrTypeInvalidParams, err) + 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 获取伪静态配置 diff --git a/backend/app/dto/request/website.go b/backend/app/dto/request/website.go index c459d81fb..3dd51ea68 100644 --- a/backend/app/dto/request/website.go +++ b/backend/app/dto/request/website.go @@ -158,6 +158,12 @@ type WebsitePHPFileUpdate struct { 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" validate:"required"` +} + type WebsiteUpdateDir struct { ID uint `json:"id" validate:"required"` SiteDir string `json:"siteDir" validate:"required"` diff --git a/backend/app/repo/app_install.go b/backend/app/repo/app_install.go index 1655b812b..a70abb442 100644 --- a/backend/app/repo/app_install.go +++ b/backend/app/repo/app_install.go @@ -126,7 +126,7 @@ func (a *AppInstallRepo) Create(ctx context.Context, install *model.AppInstall) } func (a *AppInstallRepo) Save(ctx context.Context, install *model.AppInstall) error { - return getTx(ctx).Omit(clause.Associations).Save(&install).Error + return getTx(ctx).Save(&install).Error } func (a *AppInstallRepo) DeleteBy(opts ...DBOption) error { diff --git a/backend/app/service/website.go b/backend/app/service/website.go index b5117f588..61d11401f 100644 --- a/backend/app/service/website.go +++ b/backend/app/service/website.go @@ -5,9 +5,12 @@ import ( "bytes" "context" "crypto/x509" + "encoding/json" "encoding/pem" "errors" "fmt" + "github.com/1Panel-dev/1Panel/backend/utils/compose" + "github.com/1Panel-dev/1Panel/backend/utils/env" "os" "path" "reflect" @@ -54,20 +57,25 @@ type IWebsiteService interface { 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) - PreInstallCheck(req request.WebsiteInstallCheckReq) ([]response.WebsitePreInstallCheck, error) - GetWafConfig(req request.WebsiteWafReq) (response.WebsiteWafConfig, error) - UpdateWafConfig(req request.WebsiteWafUpdate) error - UpdateNginxConfigFile(req request.WebsiteNginxUpdate) error OpWebsiteLog(req request.WebsiteLogReq) (*response.WebsiteLog, error) ChangeDefaultServer(id uint) error + PreInstallCheck(req request.WebsiteInstallCheckReq) ([]response.WebsitePreInstallCheck, error) + + GetWafConfig(req request.WebsiteWafReq) (response.WebsiteWafConfig, error) + UpdateWafConfig(req request.WebsiteWafUpdate) 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 UpdateSiteDir(req request.WebsiteUpdateDir) error @@ -1036,7 +1044,7 @@ func (w WebsiteService) GetPHPConfig(id uint) (*response.PHPConfig, error) { phpConfigPath := path.Join(appInstall.GetPath(), "conf", "php.ini") fileOp := files.NewFileOp() if !fileOp.Stat(phpConfigPath) { - return nil, buserr.WithDetail(constant.ErrFileCanNotRead, "php.ini", nil) + return nil, buserr.WithMap("ErrFileNotFound", map[string]interface{}{"name": "php.ini"}, nil) } params := make(map[string]string) configFile, err := fileOp.OpenFile(phpConfigPath) @@ -1090,7 +1098,7 @@ func (w WebsiteService) UpdatePHPConfig(req request.WebsitePHPConfigUpdate) (err phpConfigPath := path.Join(appInstall.GetPath(), "conf", "php.ini") fileOp := files.NewFileOp() if !fileOp.Stat(phpConfigPath) { - return buserr.WithDetail(constant.ErrFileCanNotRead, "php.ini", nil) + return buserr.WithMap("ErrFileNotFound", map[string]interface{}{"name": "php.ini"}, nil) } configFile, err := fileOp.OpenFile(phpConfigPath) if err != nil { @@ -1182,6 +1190,108 @@ func (w WebsiteService) UpdatePHPConfigFile(req request.WebsitePHPFileUpdate) er 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(req.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) + ) + 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 + } + if busErr = fileOp.WriteFile(composePath, strings.NewReader(appDetail.DockerCompose), 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 = appDetail.DockerCompose + + _ = 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 { diff --git a/backend/i18n/lang/en.yaml b/backend/i18n/lang/en.yaml index ac9caf314..a26950de4 100644 --- a/backend/i18n/lang/en.yaml +++ b/backend/i18n/lang/en.yaml @@ -62,6 +62,7 @@ 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!' #ssl ErrSSLCannotDelete: "The certificate is being used by the website and cannot be removed" diff --git a/backend/i18n/lang/zh-Hant.yaml b/backend/i18n/lang/zh-Hant.yaml index 08216862a..56aa410c9 100644 --- a/backend/i18n/lang/zh-Hant.yaml +++ b/backend/i18n/lang/zh-Hant.yaml @@ -62,6 +62,7 @@ ErrAppDelete: '其他網站使用此應用,無法刪除' ErrGroupIsUsed: '分組正在使用中,無法刪除' ErrBackupMatch: '該備份文件與當前網站部分數據不匹配: {{ .detail}}"' ErrBackupExist: '該備份文件對應部分原數據不存在: {{ .detail}}"' +ErrPHPResource: '本地運行環境不支持切換!' #ssl ErrSSLCannotDelete: "證書正在被網站使用,無法刪除" diff --git a/backend/i18n/lang/zh.yaml b/backend/i18n/lang/zh.yaml index 3ffb03d6c..f06c04735 100644 --- a/backend/i18n/lang/zh.yaml +++ b/backend/i18n/lang/zh.yaml @@ -62,6 +62,7 @@ ErrAppDelete: '其他网站使用此应用,无法删除' ErrGroupIsUsed: '分组正在使用中,无法删除' ErrBackupMatch: '该备份文件与当前网站部分数据不匹配 {{ .detail}}"' ErrBackupExist: '该备份文件对应部分源数据不存在 {{ .detail}}"' +ErrPHPResource: '本地运行环境不支持切换!' #ssl ErrSSLCannotDelete: "证书正在被网站使用,无法删除" diff --git a/backend/router/ro_website.go b/backend/router/ro_website.go index aae9e5a64..29b06eea1 100644 --- a/backend/router/ro_website.go +++ b/backend/router/ro_website.go @@ -45,6 +45,7 @@ func (a *WebsiteRouter) InitWebsiteRouter(Router *gin.RouterGroup) { groupRouter.GET("/php/config/:id", baseApi.GetWebsitePHPConfig) groupRouter.POST("/php/config", baseApi.UpdateWebsitePHPConfig) groupRouter.POST("/php/update", baseApi.UpdatePHPFile) + groupRouter.POST("/php/version", baseApi.ChangePHPVersion) groupRouter.POST("/rewrite", baseApi.GetRewriteConfig) groupRouter.POST("/rewrite/update", baseApi.UpdateRewriteConfig) diff --git a/cmd/server/docs/docs.go b/cmd/server/docs/docs.go index b1753a3fc..d951ca4ea 100644 --- a/cmd/server/docs/docs.go +++ b/cmd/server/docs/docs.go @@ -1,5 +1,5 @@ -// Code generated by swaggo/swag. DO NOT EDIT. - +// Package docs GENERATED BY SWAG; DO NOT EDIT +// This file was generated by swaggo/swag package docs import "github.com/swaggo/swag" @@ -10057,6 +10057,57 @@ const docTemplate = `{ } } }, + "/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": { + "BeforeFuntions": [ + { + "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": [ @@ -10206,7 +10257,7 @@ const docTemplate = `{ "tags": [ "Website" ], - "summary": "Get proxy conf", + "summary": "Get redirect conf", "parameters": [ { "description": "request", @@ -15587,6 +15638,25 @@ const docTemplate = `{ } } }, + "request.WebsitePHPVersionReq": { + "type": "object", + "required": [ + "retainConfig", + "runtimeID", + "websiteID" + ], + "properties": { + "retainConfig": { + "type": "boolean" + }, + "runtimeID": { + "type": "integer" + }, + "websiteID": { + "type": "integer" + } + } + }, "request.WebsiteProxyConfig": { "type": "object", "required": [ diff --git a/cmd/server/docs/swagger.json b/cmd/server/docs/swagger.json index 0bdab66dc..3166bc9b6 100644 --- a/cmd/server/docs/swagger.json +++ b/cmd/server/docs/swagger.json @@ -10050,6 +10050,57 @@ } } }, + "/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": { + "BeforeFuntions": [ + { + "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": [ @@ -10199,7 +10250,7 @@ "tags": [ "Website" ], - "summary": "Get proxy conf", + "summary": "Get redirect conf", "parameters": [ { "description": "request", @@ -15580,6 +15631,25 @@ } } }, + "request.WebsitePHPVersionReq": { + "type": "object", + "required": [ + "retainConfig", + "runtimeID", + "websiteID" + ], + "properties": { + "retainConfig": { + "type": "boolean" + }, + "runtimeID": { + "type": "integer" + }, + "websiteID": { + "type": "integer" + } + } + }, "request.WebsiteProxyConfig": { "type": "object", "required": [ diff --git a/cmd/server/docs/swagger.yaml b/cmd/server/docs/swagger.yaml index b189673c9..96abe232b 100644 --- a/cmd/server/docs/swagger.yaml +++ b/cmd/server/docs/swagger.yaml @@ -3133,6 +3133,19 @@ definitions: - id - type type: object + request.WebsitePHPVersionReq: + properties: + retainConfig: + type: boolean + runtimeID: + type: integer + websiteID: + type: integer + required: + - retainConfig + - runtimeID + - websiteID + type: object request.WebsiteProxyConfig: properties: cache: @@ -10103,6 +10116,39 @@ paths: 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: + BeforeFuntions: + - 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: @@ -10206,7 +10252,7 @@ paths: description: OK security: - ApiKeyAuth: [] - summary: Get proxy conf + summary: Get redirect conf tags: - Website /websites/redirect/file: diff --git a/frontend/src/api/interface/website.ts b/frontend/src/api/interface/website.ts index f08f6a08a..8006b770a 100644 --- a/frontend/src/api/interface/website.ts +++ b/frontend/src/api/interface/website.ts @@ -415,4 +415,10 @@ export namespace Website { name: string; content: string; } + + export interface PHPVersionChange { + websiteID: number; + runtimeID: number; + retainConfig: boolean; + } } diff --git a/frontend/src/api/modules/website.ts b/frontend/src/api/modules/website.ts index 338ad8927..c5ed18cfa 100644 --- a/frontend/src/api/modules/website.ts +++ b/frontend/src/api/modules/website.ts @@ -226,3 +226,7 @@ export const OperateRedirectConfig = (req: Website.WebsiteReq) => { export const UpdateRedirectConfigFile = (req: Website.RedirectFileUpdate) => { return http.post(`/websites/redirect/file`, req); }; + +export const ChangePHPVersion = (req: Website.PHPVersionChange) => { + return http.post(`/websites/php/version`, req); +}; diff --git a/frontend/src/lang/modules/en.ts b/frontend/src/lang/modules/en.ts index 7cf8ccc46..2a176aff4 100644 --- a/frontend/src/lang/modules/en.ts +++ b/frontend/src/lang/modules/en.ts @@ -1460,6 +1460,10 @@ const message = { notKeep: 'Do not keep', redirectRoot: 'Redirect to the homepage', redirectHelper: '301 permanent redirection, 302 temporary redirection', + changePHPVersionWarn: + 'Switching the PHP version will delete the original PHP container (the website code that has been mounted will not be lost), continue? ', + changeVersion: 'Switch version', + retainConfig: 'Whether to keep php-fpm.conf and php.ini files', }, php: { short_open_tag: 'Short tag support', diff --git a/frontend/src/lang/modules/tw.ts b/frontend/src/lang/modules/tw.ts index 19024c342..afb42b47c 100644 --- a/frontend/src/lang/modules/tw.ts +++ b/frontend/src/lang/modules/tw.ts @@ -1395,6 +1395,9 @@ const message = { notKeep: '不保留', redirectRoot: '重定向到首頁', redirectHelper: '301永久重定向,302臨時重定向', + changePHPVersionWarn: '切換 PHP 版本會刪除原有的 PHP 容器(不會丟失已經掛載的網站代碼),是否繼續? ', + changeVersion: '切換版本', + retainConfig: '是否保留 php-fpm.conf 和 php.ini 文件', }, php: { short_open_tag: '短標簽支持', diff --git a/frontend/src/lang/modules/zh.ts b/frontend/src/lang/modules/zh.ts index f7a6b73aa..9d3734ada 100644 --- a/frontend/src/lang/modules/zh.ts +++ b/frontend/src/lang/modules/zh.ts @@ -1395,6 +1395,9 @@ const message = { notKeep: '不保留', redirectRoot: '重定向到首页', redirectHelper: '301永久重定向,302临时重定向', + changePHPVersionWarn: '切换 PHP 版本会删除原有的 PHP 容器(不会丢失已经挂载的网站代码),是否继续?', + changeVersion: '切换版本', + retainConfig: '是否保留 php-fpm.conf 和 php.ini 文件', }, php: { short_open_tag: '短标签支持', diff --git a/frontend/src/views/website/website/config/php/index.vue b/frontend/src/views/website/website/config/php/index.vue index 6e793f057..eecdc2578 100644 --- a/frontend/src/views/website/website/config/php/index.vue +++ b/frontend/src/views/website/website/config/php/index.vue @@ -9,6 +9,9 @@ + + + @@ -19,6 +22,7 @@ import { computed, onMounted, ref } from 'vue'; import Config from './config/index.vue'; import Function from './function/index.vue'; import Upload from './upload/index.vue'; +import Version from './version/index.vue'; const props = defineProps({ id: { @@ -34,10 +38,12 @@ const id = computed(() => { const index = ref('0'); const configPHP = ref(false); const installId = ref(0); +const runtimeID = ref(0); const getWebsiteDetail = async () => { const res = await GetWebsite(props.id); if (res.data.type === 'runtime') { + runtimeID.value = res.data.runtimeID; installId.value = res.data.appInstallId; const runRes = await GetRuntime(res.data.runtimeID); if (runRes.data.resource === 'appstore') { diff --git a/frontend/src/views/website/website/config/php/version/index.vue b/frontend/src/views/website/website/config/php/version/index.vue new file mode 100644 index 000000000..942321e73 --- /dev/null +++ b/frontend/src/views/website/website/config/php/version/index.vue @@ -0,0 +1,107 @@ + + +