From d9f9f9a62916535067438baa8630783b9a545395 Mon Sep 17 00:00:00 2001 From: ssongliu <73214554+ssongliu@users.noreply.github.com> Date: Tue, 21 May 2024 16:42:46 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E7=BD=91=E7=AB=99=E5=88=9B=E5=BB=BA?= =?UTF-8?q?=E5=A2=9E=E5=8A=A0=20FTP=20=E9=80=89=E9=A1=B9=20(#5076)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/app/api/v1/ftp.go | 2 +- backend/app/api/v1/website.go | 10 +++++ backend/app/dto/request/website.go | 3 ++ backend/app/model/website.go | 1 + backend/app/service/ftp.go | 28 ++++++++---- backend/app/service/website.go | 8 ++++ backend/init/migration/migrations/v_1_10.go | 4 +- backend/utils/toolbox/pure-ftpd.go | 29 +++++++------ cmd/server/docs/docs.go | 12 ++++++ cmd/server/docs/swagger.json | 12 ++++++ cmd/server/docs/swagger.yaml | 8 ++++ frontend/src/api/interface/website.ts | 2 + frontend/src/api/modules/website.ts | 8 +++- frontend/src/global/form-rules.ts | 19 ++++++++ frontend/src/lang/modules/en.ts | 7 ++- frontend/src/lang/modules/tw.ts | 6 ++- frontend/src/lang/modules/zh.ts | 5 +++ frontend/src/views/database/mysql/index.vue | 2 +- .../src/views/database/mysql/remote/index.vue | 2 +- .../src/views/database/postgresql/index.vue | 2 +- .../database/postgresql/remote/index.vue | 2 +- .../src/views/database/redis/remote/index.vue | 2 +- frontend/src/views/toolbox/ftp/index.vue | 18 ++++++-- frontend/src/views/toolbox/ftp/log/index.vue | 23 ++++++---- .../src/views/toolbox/ftp/operate/index.vue | 21 ++++++--- .../views/website/website/create/index.vue | 43 ++++++++++++++++++- 26 files changed, 228 insertions(+), 51 deletions(-) diff --git a/backend/app/api/v1/ftp.go b/backend/app/api/v1/ftp.go index 618395f98..4241013af 100644 --- a/backend/app/api/v1/ftp.go +++ b/backend/app/api/v1/ftp.go @@ -122,7 +122,7 @@ func (b *BaseApi) CreateFtp(c *gin.Context) { } req.Password = string(pass) } - if err := ftpService.Create(req); err != nil { + if _, err := ftpService.Create(req); err != nil { helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) return } diff --git a/backend/app/api/v1/website.go b/backend/app/api/v1/website.go index 7784f7f45..30797f875 100644 --- a/backend/app/api/v1/website.go +++ b/backend/app/api/v1/website.go @@ -1,6 +1,8 @@ package v1 import ( + "encoding/base64" + "github.com/1Panel-dev/1Panel/backend/app/api/v1/helper" "github.com/1Panel-dev/1Panel/backend/app/dto" "github.com/1Panel-dev/1Panel/backend/app/dto/request" @@ -76,6 +78,14 @@ func (b *BaseApi) CreateWebsite(c *gin.Context) { 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) diff --git a/backend/app/dto/request/website.go b/backend/app/dto/request/website.go index 3cc391917..652b9138b 100644 --- a/backend/app/dto/request/website.go +++ b/backend/app/dto/request/website.go @@ -27,6 +27,9 @@ type WebsiteCreate struct { AppID uint `json:"appID"` AppInstallID uint `json:"appInstallID"` + FtpUser string `json:"ftpUser"` + FtpPassword string `json:"ftpPassword"` + RuntimeID uint `json:"runtimeID"` RuntimeConfig } diff --git a/backend/app/model/website.go b/backend/app/model/website.go index 0c21e5782..34dc72ab4 100644 --- a/backend/app/model/website.go +++ b/backend/app/model/website.go @@ -26,6 +26,7 @@ type Website struct { 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"` diff --git a/backend/app/service/ftp.go b/backend/app/service/ftp.go index 4e95e923b..879a9655f 100644 --- a/backend/app/service/ftp.go +++ b/backend/app/service/ftp.go @@ -1,6 +1,9 @@ package service import ( + "fmt" + "os" + "github.com/1Panel-dev/1Panel/backend/app/dto" "github.com/1Panel-dev/1Panel/backend/app/model" "github.com/1Panel-dev/1Panel/backend/constant" @@ -16,7 +19,7 @@ type IFtpService interface { LoadBaseInfo() (dto.FtpBaseInfo, error) SearchWithPage(search dto.SearchWithPage) (int64, interface{}, error) Operate(operation string) error - Create(req dto.FtpCreate) error + Create(req dto.FtpCreate) (uint, error) Delete(req dto.BatchDeleteReq) error Update(req dto.FtpUpdate) error Sync() error @@ -122,32 +125,39 @@ func (f *FtpService) Sync() error { return nil } -func (f *FtpService) Create(req dto.FtpCreate) error { +func (f *FtpService) Create(req dto.FtpCreate) (uint, error) { + if _, err := os.Stat(req.Path); err != nil { + if os.IsNotExist(err) { + fmt.Println(os.MkdirAll(req.Path, os.ModePerm)) + } else { + return 0, err + } + } pass, err := encrypt.StringEncrypt(req.Password) if err != nil { - return err + return 0, err } userInDB, _ := ftpRepo.Get(hostRepo.WithByUser(req.User)) if userInDB.ID != 0 { - return constant.ErrRecordExist + return 0, constant.ErrRecordExist } client, err := toolbox.NewFtpClient() if err != nil { - return err + return 0, err } if err := client.UserAdd(req.User, req.Password, req.Path); err != nil { - return err + return 0, err } var ftp model.Ftp if err := copier.Copy(&ftp, &req); err != nil { - return errors.WithMessage(constant.ErrStructTransform, err.Error()) + return 0, errors.WithMessage(constant.ErrStructTransform, err.Error()) } ftp.Status = constant.StatusEnable ftp.Password = pass if err := ftpRepo.Create(&ftp); err != nil { - return err + return 0, err } - return nil + return ftp.ID, nil } func (f *FtpService) Delete(req dto.BatchDeleteReq) error { diff --git a/backend/app/service/website.go b/backend/app/service/website.go index 94c068c0b..d54a35555 100644 --- a/backend/app/service/website.go +++ b/backend/app/service/website.go @@ -331,6 +331,14 @@ func (w WebsiteService) CreateWebsite(create request.WebsiteCreate) (err error) return err } + if len(create.FtpUser) != 0 && len(create.FtpPassword) != 0 { + itemID, err := NewIFtpService().Create(dto.FtpCreate{User: create.FtpUser, Password: create.FtpPassword, Path: path.Join(global.CONF.System.BaseDir, "1panel/apps/openresty/openresty/www/sites", create.Alias, "index")}) + 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 } diff --git a/backend/init/migration/migrations/v_1_10.go b/backend/init/migration/migrations/v_1_10.go index 2aef6c04c..638cf3741 100644 --- a/backend/init/migration/migrations/v_1_10.go +++ b/backend/init/migration/migrations/v_1_10.go @@ -205,9 +205,9 @@ var AddMonitorMenu = &gormigrate.Migration{ } var AddFtp = &gormigrate.Migration{ - ID: "20240517-add-ftp", + ID: "20240521-add-ftp", Migrate: func(tx *gorm.DB) error { - if err := tx.AutoMigrate(&model.Ftp{}); err != nil { + if err := tx.AutoMigrate(&model.Ftp{}, model.Website{}); err != nil { return err } return nil diff --git a/backend/utils/toolbox/pure-ftpd.go b/backend/utils/toolbox/pure-ftpd.go index b864a108e..cbfc22af9 100644 --- a/backend/utils/toolbox/pure-ftpd.go +++ b/backend/utils/toolbox/pure-ftpd.go @@ -16,7 +16,8 @@ import ( ) type Ftp struct { - DefaultUser string + DefaultUser string + DefaultGroup string } type FtpClient interface { @@ -33,7 +34,11 @@ type FtpClient interface { func NewFtpClient() (*Ftp, error) { userItem, err := user.LookupId("1000") if err == nil { - return &Ftp{DefaultUser: userItem.Username}, err + 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 @@ -45,7 +50,7 @@ func NewFtpClient() (*Ftp, error) { if err != nil { return nil, errors.New(stdout2) } - return &Ftp{DefaultUser: "1panel"}, nil + return &Ftp{DefaultUser: "1panel", DefaultGroup: groupItem.Name}, nil } if err.Error() != user.UnknownGroupIdError("1000").Error() { return nil, err @@ -54,11 +59,11 @@ func NewFtpClient() (*Ftp, error) { if err != nil { return nil, errors.New(string(stdout)) } - stdout2, err := cmd.Execf("useradd -u 1000 -g %s %s", groupItem.Name, userItem.Username) + stdout2, err := cmd.Execf("useradd -u 1000 -g 1panel %s", userItem.Username) if err != nil { return nil, errors.New(stdout2) } - return &Ftp{DefaultUser: "1panel"}, nil + return &Ftp{DefaultUser: "1panel", DefaultGroup: "1panel"}, nil } func (f *Ftp) Status() (bool, bool) { @@ -87,7 +92,7 @@ func (f *Ftp) UserAdd(username, passwd, path string) error { return errors.New(std) } _ = f.Reload() - std2, err := cmd.Execf("chown %s %s", f.DefaultUser, path) + std2, err := cmd.Execf("chown -R %s:%s %s", f.DefaultUser, f.DefaultGroup, path) if err != nil { return errors.New(std2) } @@ -170,19 +175,19 @@ func (f *Ftp) LoadLogs(user, operation string) ([]FtpLog, error) { logItem := "" if _, err := os.Stat("/etc/pure-ftpd/conf"); err != nil && os.IsNotExist(err) { std, err := cmd.Exec("cat /etc/pure-ftpd/pure-ftpd.conf | grep AltLog | grep clf:") - if err != nil { - return logs, err + logItem = "/var/log/pureftpd.log" + if err == nil && !strings.HasPrefix(logItem, "#") { + logItem = std } - logItem = std } else { if err != nil { return logs, err } std, err := cmd.Exec("cat /etc/pure-ftpd/conf/AltLog") - if err != nil { - return nil, err + logItem = "/var/log/pure-ftpd/transfer.log" + if err != nil && !strings.HasPrefix(logItem, "#") { + logItem = std } - logItem = std } logItem = strings.ReplaceAll(logItem, "AltLog", "") diff --git a/cmd/server/docs/docs.go b/cmd/server/docs/docs.go index 164d718ec..a84a97365 100644 --- a/cmd/server/docs/docs.go +++ b/cmd/server/docs/docs.go @@ -19029,6 +19029,9 @@ const docTemplate = `{ "expireDate": { "type": "string" }, + "ftpId": { + "type": "integer" + }, "group": { "type": "string" }, @@ -20820,6 +20823,12 @@ const docTemplate = `{ "installed" ] }, + "ftpPassword": { + "type": "string" + }, + "ftpUser": { + "type": "string" + }, "otherDomains": { "type": "string" }, @@ -22062,6 +22071,9 @@ const docTemplate = `{ "expireDate": { "type": "string" }, + "ftpId": { + "type": "integer" + }, "group": { "type": "string" }, diff --git a/cmd/server/docs/swagger.json b/cmd/server/docs/swagger.json index a766883e5..a75e7e809 100644 --- a/cmd/server/docs/swagger.json +++ b/cmd/server/docs/swagger.json @@ -19022,6 +19022,9 @@ "expireDate": { "type": "string" }, + "ftpId": { + "type": "integer" + }, "group": { "type": "string" }, @@ -20813,6 +20816,12 @@ "installed" ] }, + "ftpPassword": { + "type": "string" + }, + "ftpUser": { + "type": "string" + }, "otherDomains": { "type": "string" }, @@ -22055,6 +22064,9 @@ "expireDate": { "type": "string" }, + "ftpId": { + "type": "integer" + }, "group": { "type": "string" }, diff --git a/cmd/server/docs/swagger.yaml b/cmd/server/docs/swagger.yaml index 3e8b39eae..d1ef2bc53 100644 --- a/cmd/server/docs/swagger.yaml +++ b/cmd/server/docs/swagger.yaml @@ -3050,6 +3050,8 @@ definitions: type: boolean expireDate: type: string + ftpId: + type: integer group: type: string httpConfig: @@ -4248,6 +4250,10 @@ definitions: - new - installed type: string + ftpPassword: + type: string + ftpUser: + type: string otherDomains: type: string port: @@ -5080,6 +5086,8 @@ definitions: type: string expireDate: type: string + ftpId: + type: integer group: type: string httpConfig: diff --git a/frontend/src/api/interface/website.ts b/frontend/src/api/interface/website.ts index 2b1538d1f..25ed25f4e 100644 --- a/frontend/src/api/interface/website.ts +++ b/frontend/src/api/interface/website.ts @@ -64,6 +64,8 @@ export namespace Website { otherDomains: string; proxy: string; proxyType: string; + ftpUser: string; + ftpPassword: string; } export interface WebSiteUpdateReq { diff --git a/frontend/src/api/modules/website.ts b/frontend/src/api/modules/website.ts index 9452c1bc4..becc731f1 100644 --- a/frontend/src/api/modules/website.ts +++ b/frontend/src/api/modules/website.ts @@ -3,6 +3,8 @@ import { ReqPage, ResPage } from '../interface'; import { Website } from '../interface/website'; import { File } from '../interface/file'; import { TimeoutEnum } from '@/enums/http-enum'; +import { deepCopy } from '@/utils/util'; +import { Base64 } from 'js-base64'; export const SearchWebsites = (req: Website.WebSiteSearch) => { return http.post>(`/websites/search`, req); @@ -13,7 +15,11 @@ export const ListWebsites = () => { }; export const CreateWebsite = (req: Website.WebSiteCreateReq) => { - return http.post(`/websites`, req); + let request = deepCopy(req) as Website.WebSiteCreateReq; + if (request.ftpPassword) { + request.ftpPassword = Base64.encode(request.ftpPassword); + } + return http.post(`/websites`, request); }; export const OpWebsite = (req: Website.WebSiteOp) => { diff --git a/frontend/src/global/form-rules.ts b/frontend/src/global/form-rules.ts index 50fe6ead5..4b65b2a0e 100644 --- a/frontend/src/global/form-rules.ts +++ b/frontend/src/global/form-rules.ts @@ -181,6 +181,19 @@ const checkSimpleName = (rule: any, value: any, callback: any) => { } }; +const checkSimplePassword = (rule: any, value: any, callback: any) => { + if (value === '' || typeof value === 'undefined' || value == null) { + callback(new Error(i18n.global.t('commons.rule.simplePassword'))); + } else { + const reg = /^[a-zA-Z0-9]{1}[a-zA-Z0-9_]{5,29}$/; + if (!reg.test(value) && value !== '') { + callback(new Error(i18n.global.t('commons.rule.simplePassword'))); + } else { + callback(); + } + } +}; + const checkDBName = (rule: any, value: any, callback: any) => { if (value === '' || typeof value === 'undefined' || value == null) { callback(new Error(i18n.global.t('commons.rule.dbName'))); @@ -535,6 +548,7 @@ interface CommonRule { name: FormItemRule; userName: FormItemRule; simpleName: FormItemRule; + simplePassword: FormItemRule; dbName: FormItemRule; imageName: FormItemRule; volumeName: FormItemRule; @@ -601,6 +615,11 @@ export const Rules: CommonRule = { validator: checkSimpleName, trigger: 'blur', }, + simplePassword: { + required: true, + validator: checkSimplePassword, + trigger: 'blur', + }, dbName: { required: true, validator: checkDBName, diff --git a/frontend/src/lang/modules/en.ts b/frontend/src/lang/modules/en.ts index 675a84a95..2f05bdf0b 100644 --- a/frontend/src/lang/modules/en.ts +++ b/frontend/src/lang/modules/en.ts @@ -170,7 +170,8 @@ const message = { commonName: 'Supports non-special characters starting with English, Chinese, numbers, .- and _, length 1-128', userName: 'Support English, Chinese, numbers and _ length 3-30', - simpleName: 'Supports non-underscore starting, English, numbers, _, length 1-30', + simpleName: 'Supports non-underscore starting, English, numbers, _, length 3-30', + simplePassword: 'Supports non-underscore starting, English, numbers, _, length 6-30', dbName: 'Supports non-special character starting, including English, Chinese, numbers, .-_, with a length of 1-64', imageName: 'Support English, numbers, :/.-_, length 1-150', volumeName: 'Support English, numbers, .-_, length 2-30', @@ -1736,6 +1737,10 @@ const message = { zipFormat: '.tar.gz compressed package structure: test.tar.gz compressed package must contain {0} file', proxy: 'Reverse Proxy', alias: 'Path Name', + ftpUser: 'FTP Account', + ftpPassword: 'FTP Password', + ftpHelper: + 'Create an FTP account corresponding to the site while creating the site, and the FTP directory points to the directory where the site is located.', remark: 'Remark', group: 'Group', groupSetting: 'Group Management', diff --git a/frontend/src/lang/modules/tw.ts b/frontend/src/lang/modules/tw.ts index 546719a93..70fa93699 100644 --- a/frontend/src/lang/modules/tw.ts +++ b/frontend/src/lang/modules/tw.ts @@ -170,7 +170,8 @@ const message = { illegalInput: '輸入框中存在不合法字符', commonName: '支持非特殊字元開頭,英文、中文、數字、.-和_,長度1-128', userName: '支持英文、中文、數字和_,長度3-30', - simpleName: '支持非底線開頭,英文、數字、_,長度1-30', + simpleName: '支持非底線開頭,英文、數字、_,長度3-30', + simplePassword: '支持非底線開頭,英文、數字、_,長度6-30', dbName: '支持非特殊字符開頭,英文、中文、數字、.-_,長度1-64', imageName: '支持英文、數字、:/.-_,長度1-150', volumeName: '支持英文、數字、.-和_,長度2-30', @@ -1617,6 +1618,9 @@ const message = { zipFormat: '.tar.gz 壓縮包結構:test.tar.gz 壓縮包內,必需包含 {0} 文件', proxy: '反向代理', alias: '代號', + ftpUser: 'FTP 帳號', + ftpPassword: 'FTP 密碼', + ftpHelper: '建立站點的同時,為站點建立一個對應 FTP 帳戶,並且 FTP 目錄指向站點所在目錄。', remark: '備註', group: '分組', groupSetting: '分組管理', diff --git a/frontend/src/lang/modules/zh.ts b/frontend/src/lang/modules/zh.ts index b9671d3a0..deeb822ff 100644 --- a/frontend/src/lang/modules/zh.ts +++ b/frontend/src/lang/modules/zh.ts @@ -171,6 +171,7 @@ const message = { commonName: '支持非特殊字符开头,英文、中文、数字、.-和_,长度1-128', userName: '支持英文、中文、数字和_,长度3-30', simpleName: '支持非下划线开头,英文、数字、_,长度3-30', + simplePassword: '支持非下划线开头,英文、数字、_,长度6-30', dbName: '支持非特殊字符开头,英文、中文、数字、.-_,长度1-64', imageName: '支持英文、数字、:/.-_,长度1-150', volumeName: '支持英文、数字、.-和_,长度2-30', @@ -1617,6 +1618,10 @@ const message = { zipFormat: '.tar.gz 压缩包结构:test.tar.gz 压缩包内,必需包含 {0} 文件', proxy: '反向代理', alias: '代号', + enableFtp: '创建 FTP', + ftpUser: 'FTP 账号', + ftpPassword: 'FTP 密码', + ftpHelper: '创建站点的同时,为站点创建一个对应 FTP 帐户,并且 FTP 目录指向站点所在目录。', remark: '备注', group: '分组', groupSetting: '分组管理', diff --git a/frontend/src/views/database/mysql/index.vue b/frontend/src/views/database/mysql/index.vue index b9c0f65d9..f4f127289 100644 --- a/frontend/src/views/database/mysql/index.vue +++ b/frontend/src/views/database/mysql/index.vue @@ -154,7 +154,7 @@