feat: separate website and standalone FTP identities (#13412)

This commit is contained in:
ssongliu
2026-07-29 16:41:43 +08:00
committed by GitHub
parent 357d77856a
commit 13e6bc4fac
18 changed files with 325 additions and 285 deletions
-15
View File
@@ -24,21 +24,6 @@ func (b *BaseApi) LoadFtpBaseInfo(c *gin.Context) {
helper.SuccessWithData(c, data)
}
// @Tags FTP
// @Summary Initialize FTP identity
// @Success 200
// @Security ApiKeyAuth
// @Security Timestamp
// @Router /toolbox/ftp/init [post]
// @x-panel-log {"bodyKeys":[],"paramKeys":[],"BeforeFunctions":[],"formatZH":"初始化 FTP 用户身份","formatEN":"initialize FTP user identity"}
func (b *BaseApi) InitFtp(c *gin.Context) {
if err := ftpService.Init(); err != nil {
helper.InternalServer(c, err)
return
}
helper.Success(c)
}
// @Tags FTP
// @Summary Load FTP operation log
// @Accept json
-1
View File
@@ -18,7 +18,6 @@ type FtpInfo struct {
type FtpBaseInfo struct {
IsActive bool `json:"isActive"`
IsExist bool `json:"isExist"`
IsInit bool `json:"isInit"`
}
type FtpLogSearch struct {
+2
View File
@@ -8,4 +8,6 @@ type Ftp struct {
Status string `gorm:"not null" json:"status"`
Path string `gorm:"not null" json:"path"`
Description string `gorm:"not null" json:"description"`
UID uint `gorm:"column:uid;not null;default:1000" json:"-"`
GID uint `gorm:"column:gid;not null;default:1000" json:"-"`
}
+46 -21
View File
@@ -1,6 +1,8 @@
package service
import (
"errors"
"fmt"
"os"
"sort"
@@ -19,10 +21,10 @@ type FtpService struct{}
type IFtpService interface {
LoadBaseInfo() (dto.FtpBaseInfo, error)
Init() error
SearchWithPage(search dto.SearchWithPage) (int64, interface{}, error)
Operate(operation string) error
Create(req dto.FtpCreate) (uint, error)
CreateWebsite(req dto.FtpCreate) (uint, error)
Delete(req dto.BatchDeleteReq) error
Update(req dto.FtpUpdate) error
Sync() error
@@ -35,19 +37,10 @@ func NewIFtpService() IFtpService {
func (f *FtpService) LoadBaseInfo() (dto.FtpBaseInfo, error) {
var baseInfo dto.FtpBaseInfo
isInit, err := toolbox.IsFtpInitialized()
if err != nil {
return baseInfo, err
}
baseInfo.IsInit = isInit
baseInfo.IsActive, baseInfo.IsExist = toolbox.FtpStatus()
return baseInfo, nil
}
func (f *FtpService) Init() error {
return toolbox.InitFtp()
}
func (f *FtpService) LoadLog(req dto.FtpLogSearch) (int64, interface{}, error) {
client, err := toolbox.NewFtpClient()
if err != nil {
@@ -82,9 +75,6 @@ func (u *FtpService) Operate(operation string) error {
}
func (f *FtpService) SearchWithPage(req dto.SearchWithPage) (int64, interface{}, error) {
if _, err := toolbox.NewFtpClient(); err != nil {
return 0, nil, err
}
total, lists, err := ftpRepo.Page(req.Page, req.PageSize, ftpRepo.WithLikeUser(req.Info), repo.WithOrderDesc("created_at"))
if err != nil {
return 0, nil, err
@@ -108,7 +98,7 @@ func (f *FtpService) Sync() error {
}
lists, err := client.LoadList()
if err != nil {
return nil
return err
}
listsInDB, err := ftpRepo.GetList()
if err != nil {
@@ -122,13 +112,24 @@ func (f *FtpService) Sync() error {
for _, item := range lists {
if itemInDB, ok := currentData[item.User]; ok {
sameData[item.User] = struct{}{}
if item.Path != itemInDB.Path || item.Status != itemInDB.Status {
if err := ftpRepo.Update(itemInDB.ID, map[string]interface{}{"path": item.Path, "status": item.Status}); err != nil {
if item.Path != itemInDB.Path || item.Status != itemInDB.Status || item.UID != itemInDB.UID || item.GID != itemInDB.GID {
if err := ftpRepo.Update(itemInDB.ID, map[string]interface{}{
"path": item.Path,
"status": item.Status,
"uid": item.UID,
"gid": item.GID,
}); err != nil {
return err
}
}
} else {
if err := ftpRepo.Create(&model.Ftp{User: item.User, Path: item.Path, Status: item.Status}); err != nil {
if err := ftpRepo.Create(&model.Ftp{
User: item.User,
Path: item.Path,
Status: item.Status,
UID: item.UID,
GID: item.GID,
}); err != nil {
return err
}
}
@@ -142,6 +143,14 @@ func (f *FtpService) Sync() error {
}
func (f *FtpService) Create(req dto.FtpCreate) (uint, error) {
return f.create(req, false)
}
func (f *FtpService) CreateWebsite(req dto.FtpCreate) (uint, error) {
return f.create(req, true)
}
func (f *FtpService) create(req dto.FtpCreate, website bool) (uint, error) {
if err := toolbox.ValidateFtpRootPath(req.Path); err != nil {
return 0, err
}
@@ -166,16 +175,28 @@ func (f *FtpService) Create(req dto.FtpCreate) (uint, error) {
if userInDB.ID != 0 {
return 0, buserr.New("ErrRecordExist")
}
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, buserr.WithDetail("ErrStructTransform", err.Error(), nil)
}
uid, gid := uint(constant.WebsiteUID), uint(constant.WebsiteGID)
if !website {
uid, gid, err = toolbox.EnsureStandaloneFtpIdentity()
if err != nil {
return 0, err
}
}
if err := client.UserAdd(req.User, req.Password, req.Path, uid, gid); err != nil {
return 0, err
}
ftp.Status = constant.StatusEnable
ftp.Password = pass
ftp.UID = uid
ftp.GID = gid
if err := ftpRepo.Create(&ftp); err != nil {
if rollbackErr := client.UserDel(req.User); rollbackErr != nil {
return 0, errors.Join(err, fmt.Errorf("rollback FTP user %s failed: %w", req.User, rollbackErr))
}
return 0, err
}
return ftp.ID, nil
@@ -245,7 +266,11 @@ func (f *FtpService) Update(req dto.FtpUpdate) error {
needReload = true
}
if req.Path != ftpItem.Path {
if err := client.SetPath(ftpItem.User, req.Path); err != nil {
uid, gid := ftpItem.UID, ftpItem.GID
if uid == 0 || gid == 0 {
uid, gid = uint(constant.WebsiteUID), uint(constant.WebsiteGID)
}
if err := client.SetPath(ftpItem.User, req.Path, uid, gid); err != nil {
return err
}
updates["path"] = req.Path
+1 -1
View File
@@ -561,7 +561,7 @@ func (w WebsiteService) CreateWebsite(create request.WebsiteCreate) (err error)
if len(create.FtpUser) != 0 && len(create.FtpPassword) != 0 {
createFtpUser := func(t *task.Task) error {
indexDir := GetSitePath(*website, SiteIndexDir)
itemID, err := NewIFtpService().Create(dto.FtpCreate{User: create.FtpUser, Password: create.FtpPassword, Path: indexDir})
itemID, err := NewIFtpService().CreateWebsite(dto.FtpCreate{User: create.FtpUser, Password: create.FtpPassword, Path: indexDir})
if err != nil {
return err
}
+1 -1
View File
@@ -1221,7 +1221,7 @@ func checkIsLinkApp(website model.Website) bool {
func chownRootDir(path string) error {
cmdMgr := cmd.NewCommandMgr(cmd.WithTimeout(1 * time.Second))
owner := fmt.Sprintf("%d:%d", constant.FTPUid, constant.FTPGid)
owner := fmt.Sprintf("%d:%d", constant.WebsiteUID, constant.WebsiteGID)
if err := cmdMgr.Run("chown", "-R", owner, path); err != nil {
return err
}
-7
View File
@@ -4166,13 +4166,6 @@
"formatZH": "删除 FTP 账户 [users]",
"formatEN": "delete FTP users [users]"
},
"/toolbox/ftp/init": {
"bodyKeys": [],
"paramKeys": [],
"beforeFunctions": [],
"formatZH": "初始化 FTP 用户身份",
"formatEN": "initialize FTP user identity"
},
"/toolbox/ftp/operate": {
"bodyKeys": [
"operation"
+4 -3
View File
@@ -6,7 +6,8 @@ const (
SupervisorConfigPath = "SupervisorConfigPath"
SupervisorServiceName = "SupervisorServiceName"
FTPUser = "1panel"
FTPUid = 1000
FTPGid = 1000
WebsiteUID = 1000
WebsiteGID = 1000
FTPUser = "1panel-ftp"
)
+1
View File
@@ -94,6 +94,7 @@ func InitAgentDB() {
migrations.InitFirewallPortWhiteList,
migrations.AddDatabaseUserTable,
migrations.AddBackupRecordArgs,
migrations.AddFtpIdentity,
})
if err := m.Migrate(); err != nil {
global.LOG.Error(err)
+15
View File
@@ -1683,3 +1683,18 @@ var AddBackupRecordArgs = &gormigrate.Migration{
return tx.AutoMigrate(&model.BackupRecord{})
},
}
var AddFtpIdentity = &gormigrate.Migration{
ID: "20260729-add-ftp-identity",
Migrate: func(tx *gorm.DB) error {
if err := tx.AutoMigrate(&model.Ftp{}); err != nil {
return err
}
return tx.Model(&model.Ftp{}).
Where("1 = 1").
Updates(map[string]interface{}{
"uid": constant.WebsiteUID,
"gid": constant.WebsiteGID,
}).Error
},
}
-1
View File
@@ -34,7 +34,6 @@ func (s *ToolboxRouter) InitRouter(Router *gin.RouterGroup) {
toolboxRouter.POST("/fail2ban/update/byconf", baseApi.UpdateFail2BanConfByFile)
toolboxRouter.GET("/ftp/base", baseApi.LoadFtpBaseInfo)
toolboxRouter.POST("/ftp/init", baseApi.InitFtp)
toolboxRouter.POST("/ftp/log/search", baseApi.LoadFtpLogInfo)
toolboxRouter.POST("/ftp/operate", baseApi.OperateFtp)
toolboxRouter.POST("/ftp/search", baseApi.SearchFtp)
+228 -98
View File
@@ -27,6 +27,8 @@ type FtpList struct {
User string
Path string
Status string
UID uint
GID uint
}
type FtpLog struct {
@@ -42,19 +44,14 @@ type FtpClient interface {
Status() (bool, bool)
Operate(operate string) error
LoadList() ([]FtpList, error)
UserAdd(username, path, passwd string) error
UserAdd(username, passwd, path string, uid, gid uint) error
UserDel(username string) error
SetPasswd(username, passwd string) error
SetPath(username, path string, uid, gid uint) error
Reload() error
LoadLogs() ([]FtpLog, error)
}
var ErrFtpNotInitialized = fmt.Errorf(
"FTP identity %d:%d is not initialized",
constant.FTPUid,
constant.FTPGid,
)
var ErrFtpUnsafePath = errors.New("FTP root path is unsafe")
var ftpUnsafeRootPaths = map[string]struct{}{
@@ -76,63 +73,93 @@ var ftpUnsafeRootPaths = map[string]struct{}{
"/sys": {},
}
var ftpInitMu sync.Mutex
var ftpIdentityMu sync.Mutex
func IsFtpInitialized() (bool, error) {
userExists, groupExists, err := loadFtpIdentityStatus()
if err != nil {
return false, err
const (
standaloneFTPMinID = 10000
standaloneFTPMaxID = 60000
)
func EnsureStandaloneFtpIdentity() (uint, uint, error) {
ftpIdentityMu.Lock()
defer ftpIdentityMu.Unlock()
userItem, userErr := user.Lookup(constant.FTPUser)
if userErr != nil && !isUnknownUser(userErr) {
return 0, 0, userErr
}
return userExists && groupExists, nil
}
func InitFtp() error {
ftpInitMu.Lock()
defer ftpInitMu.Unlock()
uid := strconv.Itoa(constant.FTPUid)
gid := strconv.Itoa(constant.FTPGid)
userExists, groupExists, err := loadFtpIdentityStatus()
if err != nil {
return err
groupItem, groupErr := user.LookupGroup(constant.FTPUser)
if groupErr != nil && !isUnknownGroup(groupErr) {
return 0, 0, groupErr
}
if !userExists {
userItem, err := user.Lookup(constant.FTPUser)
if err == nil {
if userItem.Uid != uid {
return fmt.Errorf("user %s already exists with UID %s", constant.FTPUser, userItem.Uid)
}
userExists = true
} else {
var unknownUser user.UnknownUserError
if !errors.As(err, &unknownUser) {
return err
}
if groupErr == nil {
gid, err := strconv.ParseUint(groupItem.Gid, 10, 32)
if err != nil {
return 0, 0, err
}
if gid < standaloneFTPMinID || gid > standaloneFTPMaxID {
return 0, 0, fmt.Errorf(
"FTP group %s must use a GID between %d and %d, got %d",
constant.FTPUser,
standaloneFTPMinID,
standaloneFTPMaxID,
gid,
)
}
}
if !groupExists {
groupItem, err := user.LookupGroup(constant.FTPUser)
if err == nil {
if groupItem.Gid != gid {
return fmt.Errorf("group %s already exists with GID %s", constant.FTPUser, groupItem.Gid)
if groupErr != nil {
groupID := ""
if userErr == nil {
uid, err := strconv.ParseUint(userItem.Uid, 10, 32)
if err != nil {
return 0, 0, err
}
groupExists = true
} else {
var unknownGroup user.UnknownGroupError
gid, err := strconv.ParseUint(userItem.Gid, 10, 32)
if err != nil {
return 0, 0, err
}
if uid < standaloneFTPMinID || uid > standaloneFTPMaxID ||
gid < standaloneFTPMinID || gid > standaloneFTPMaxID {
return 0, 0, fmt.Errorf(
"FTP user %s must use UID and GID between %d and %d, got %d:%d",
constant.FTPUser,
standaloneFTPMinID,
standaloneFTPMaxID,
uid,
gid,
)
}
groupByID, err := user.LookupGroupId(userItem.Gid)
if err == nil {
return 0, 0, fmt.Errorf(
"FTP user %s uses GID %s owned by group %s",
constant.FTPUser,
userItem.Gid,
groupByID.Name,
)
}
var unknownGroup user.UnknownGroupIdError
if !errors.As(err, &unknownGroup) {
return err
return 0, 0, err
}
groupID = userItem.Gid
} else {
identityID, err := findAvailableFtpIdentityID(true, true)
if err != nil {
return 0, 0, err
}
groupID = identityID
}
if err := cmd.NewCommandMgr().Run("groupadd", "-g", groupID, constant.FTPUser); err != nil {
return 0, 0, err
}
}
cmdMgr := cmd.NewCommandMgr()
if !groupExists {
if err := cmdMgr.Run("groupadd", "-g", gid, constant.FTPUser); err != nil {
return err
if userErr != nil {
uid, err := findAvailableFtpIdentityID(true, false)
if err != nil {
return 0, 0, err
}
}
if !userExists {
noLoginShell := "/bin/false"
for _, item := range []string{"/usr/sbin/nologin", "/sbin/nologin"} {
if _, err := os.Stat(item); err == nil {
@@ -140,56 +167,96 @@ func InitFtp() error {
break
}
}
if err := cmdMgr.Run(
if err := cmd.NewCommandMgr().Run(
"useradd",
"-u", uid,
"-g", gid,
"-g", constant.FTPUser,
"-M",
"-d", "/nonexistent",
"-s", noLoginShell,
constant.FTPUser,
); err != nil {
return err
return 0, 0, err
}
}
isInitialized, err := IsFtpInitialized()
userItem, err := user.Lookup(constant.FTPUser)
if err != nil {
return err
return 0, 0, err
}
if !isInitialized {
return ErrFtpNotInitialized
groupItem, err = user.LookupGroup(constant.FTPUser)
if err != nil {
return 0, 0, err
}
return nil
if userItem.Gid != groupItem.Gid {
return 0, 0, fmt.Errorf(
"FTP user %s has GID %s, expected group GID %s",
constant.FTPUser,
userItem.Gid,
groupItem.Gid,
)
}
uid, err := strconv.ParseUint(userItem.Uid, 10, 32)
if err != nil {
return 0, 0, err
}
gid, err := strconv.ParseUint(groupItem.Gid, 10, 32)
if err != nil {
return 0, 0, err
}
if uid < standaloneFTPMinID || uid > standaloneFTPMaxID ||
gid < standaloneFTPMinID || gid > standaloneFTPMaxID {
return 0, 0, fmt.Errorf(
"FTP identity %s must use UID and GID between %d and %d, got %d:%d",
constant.FTPUser,
standaloneFTPMinID,
standaloneFTPMaxID,
uid,
gid,
)
}
return uint(uid), uint(gid), nil
}
func loadFtpIdentityStatus() (bool, bool, error) {
uid := strconv.Itoa(constant.FTPUid)
_, userErr := user.LookupId(uid)
if userErr != nil {
var unknownUser user.UnknownUserIdError
if !errors.As(userErr, &unknownUser) {
return false, false, userErr
func findAvailableFtpIdentityID(checkUser, checkGroup bool) (string, error) {
for id := standaloneFTPMinID; id <= standaloneFTPMaxID; id++ {
idItem := strconv.Itoa(id)
if checkUser {
if _, err := user.LookupId(idItem); err == nil {
continue
} else {
var unknownUser user.UnknownUserIdError
if !errors.As(err, &unknownUser) {
return "", err
}
}
}
if checkGroup {
if _, err := user.LookupGroupId(idItem); err == nil {
continue
} else {
var unknownGroup user.UnknownGroupIdError
if !errors.As(err, &unknownGroup) {
return "", err
}
}
}
return idItem, nil
}
return "", fmt.Errorf("no available FTP identity ID between %d and %d", standaloneFTPMinID, standaloneFTPMaxID)
}
gid := strconv.Itoa(constant.FTPGid)
_, groupErr := user.LookupGroupId(gid)
if groupErr != nil {
var unknownGroup user.UnknownGroupIdError
if !errors.As(groupErr, &unknownGroup) {
return false, false, groupErr
}
}
return userErr == nil, groupErr == nil, nil
func isUnknownUser(err error) bool {
var unknownUser user.UnknownUserError
return errors.As(err, &unknownUser)
}
func isUnknownGroup(err error) bool {
var unknownGroup user.UnknownGroupError
return errors.As(err, &unknownGroup)
}
func NewFtpClient() (*Ftp, error) {
isInitialized, err := IsFtpInitialized()
if err != nil {
return nil, err
}
if !isInitialized {
return nil, ErrFtpNotInitialized
}
return &Ftp{}, nil
}
@@ -216,14 +283,14 @@ func (f *Ftp) Operate(operate string) error {
}
}
func (f *Ftp) UserAdd(username, passwd, path string) error {
func (f *Ftp) UserAdd(username, passwd, path string, uid, gid uint) error {
if cmd.CheckIllegal(username, path) {
return buserr.New("ErrCmdIllegal")
}
if err := ValidateFtpRootPath(path); err != nil {
return err
}
entry, err := generatePureFtpEntrySimple(username, passwd, path)
entry, err := generatePureFtpEntry(username, passwd, path, uid, gid)
if err != nil {
return fmt.Errorf("generate pure-ftpd entry failed, err: %v", err)
}
@@ -231,20 +298,31 @@ func (f *Ftp) UserAdd(username, passwd, path string) error {
if err != nil {
return err
}
defer pwdFile.Close()
_, err = pwdFile.WriteString("\n" + entry + "\n")
if err != nil {
_ = pwdFile.Close()
return err
}
_ = f.Reload()
owner := fmt.Sprintf("%d:%d", constant.FTPUid, constant.FTPGid)
if err := cmd.NewCommandMgr().Run("chown", "-R", owner, "--", path); err != nil {
if err := pwdFile.Close(); err != nil {
return err
}
if err := f.Reload(); err != nil {
return f.rollbackAddedUser(username, fmt.Errorf("reload FTP database after adding user failed: %w", err))
}
if err := chownFtpRoot(path, uid, gid); err != nil {
return f.rollbackAddedUser(username, fmt.Errorf("change FTP root ownership failed: %w", err))
}
return nil
}
func (f *Ftp) rollbackAddedUser(username string, cause error) error {
if rollbackErr := f.UserDel(username); rollbackErr != nil {
return errors.Join(cause, fmt.Errorf("rollback FTP user %s failed: %w", username, rollbackErr))
}
return cause
}
func (f *Ftp) UserDel(username string) error {
if cmd.CheckIllegal(username) {
return buserr.New("ErrCmdIllegal")
@@ -252,8 +330,7 @@ func (f *Ftp) UserDel(username string) error {
if err := cmd.NewCommandMgr().Run("pure-pw", "userdel", username); err != nil {
return err
}
_ = f.Reload()
return nil
return f.Reload()
}
func (f *Ftp) SetPasswd(username, passwd string) error {
@@ -306,7 +383,7 @@ func (f *Ftp) SetPasswd(username, passwd string) error {
return nil
}
func (f *Ftp) SetPath(username, path string) error {
func (f *Ftp) SetPath(username, path string, uid, gid uint) error {
if cmd.CheckIllegal(username, path) {
return buserr.New("ErrCmdIllegal")
}
@@ -316,13 +393,17 @@ func (f *Ftp) SetPath(username, path string) error {
if err := cmd.NewCommandMgr().Run("pure-pw", "usermod", username, "-d", path); err != nil {
return err
}
owner := fmt.Sprintf("%d:%d", constant.FTPUid, constant.FTPGid)
if err := cmd.NewCommandMgr().Run("chown", "-R", owner, "--", path); err != nil {
if err := chownFtpRoot(path, uid, gid); err != nil {
return err
}
return nil
}
func chownFtpRoot(rootPath string, uid, gid uint) error {
owner := fmt.Sprintf("%d:%d", uid, gid)
return cmd.NewCommandMgr().Run("chown", "-R", owner, "--", rootPath)
}
func ValidateFtpRootPath(rootPath string) error {
if strings.TrimSpace(rootPath) == "" {
return fmt.Errorf("%w: path is required", ErrFtpUnsafePath)
@@ -368,6 +449,10 @@ func (f *Ftp) LoadList() ([]FtpList, error) {
if err != nil {
return nil, err
}
identities, err := loadPureFtpIdentities("/etc/pure-ftpd/pureftpd.passwd")
if err != nil {
return nil, err
}
var lists []FtpList
lines := strings.Split(std, "\n")
for _, line := range lines {
@@ -391,11 +476,53 @@ func (f *Ftp) LoadList() ([]FtpList, error) {
if len(strings.TrimSpace(strings.ReplaceAll(allowedLine, "Allowed client IPs :", ""))) == 0 {
status = constant.StatusEnable
}
lists = append(lists, FtpList{User: parts[0], Path: strings.ReplaceAll(parts[1], "/./", ""), Status: status})
identity, ok := identities[parts[0]]
if !ok {
return nil, fmt.Errorf("FTP identity for user %s was not found", parts[0])
}
lists = append(lists, FtpList{
User: parts[0],
Path: strings.ReplaceAll(parts[1], "/./", ""),
Status: status,
UID: identity.UID,
GID: identity.GID,
})
}
return lists, nil
}
type ftpIdentity struct {
UID uint
GID uint
}
func loadPureFtpIdentities(passwdPath string) (map[string]ftpIdentity, error) {
pwdFile, err := os.Open(passwdPath)
if err != nil {
return nil, err
}
defer pwdFile.Close()
identities := make(map[string]ftpIdentity)
scanner := bufio.NewScanner(pwdFile)
for scanner.Scan() {
parts := strings.Split(scanner.Text(), ":")
if len(parts) < 6 || parts[0] == "" {
continue
}
uid, uidErr := strconv.ParseUint(parts[2], 10, 32)
gid, gidErr := strconv.ParseUint(parts[3], 10, 32)
if uidErr != nil || gidErr != nil {
continue
}
identities[parts[0]] = ftpIdentity{UID: uint(uid), GID: uint(gid)}
}
if err := scanner.Err(); err != nil {
return nil, err
}
return identities, nil
}
func (f *Ftp) Reload() error {
if err := cmd.NewCommandMgr().Run("pure-pw", "mkdb"); err != nil {
return err
@@ -508,7 +635,10 @@ func loadLogsByFiles(fileList []string, user, operation string) []FtpLog {
return logs
}
func generatePureFtpEntrySimple(username, password, path string) (string, error) {
func generatePureFtpEntry(username, password, path string, uid, gid uint) (string, error) {
if uid == 0 || gid == 0 {
return "", errors.New("FTP UID and GID must be greater than zero")
}
passwdAfterSha512, err := helper.Generate([]byte(password))
if err != nil {
return "", err
@@ -517,8 +647,8 @@ func generatePureFtpEntrySimple(username, password, path string) (string, error)
"%s:%s:%d:%d::%s/./::::::::::::",
username,
passwdAfterSha512,
constant.FTPUid,
constant.FTPGid,
uid,
gid,
path,
), nil
}
-31
View File
@@ -24361,34 +24361,6 @@ const docTemplate = `{
}
}
},
"/toolbox/ftp/init": {
"post": {
"responses": {
"200": {
"description": "OK"
}
},
"security": [
{
"ApiKeyAuth": []
},
{
"Timestamp": []
}
],
"summary": "Initialize FTP identity",
"tags": [
"FTP"
],
"x-panel-log": {
"BeforeFunctions": [],
"bodyKeys": [],
"formatEN": "initialize FTP user identity",
"formatZH": "初始化 FTP 用户身份",
"paramKeys": []
}
}
},
"/toolbox/ftp/log/search": {
"post": {
"consumes": [
@@ -34709,9 +34681,6 @@ const docTemplate = `{
},
"isExist": {
"type": "boolean"
},
"isInit": {
"type": "boolean"
}
},
"type": "object"
-31
View File
@@ -24357,34 +24357,6 @@
}
}
},
"/toolbox/ftp/init": {
"post": {
"responses": {
"200": {
"description": "OK"
}
},
"security": [
{
"ApiKeyAuth": []
},
{
"Timestamp": []
}
],
"summary": "Initialize FTP identity",
"tags": [
"FTP"
],
"x-panel-log": {
"BeforeFunctions": [],
"bodyKeys": [],
"formatEN": "initialize FTP user identity",
"formatZH": "初始化 FTP 用户身份",
"paramKeys": []
}
}
},
"/toolbox/ftp/log/search": {
"post": {
"consumes": [
@@ -34705,9 +34677,6 @@
},
"isExist": {
"type": "boolean"
},
"isInit": {
"type": "boolean"
}
},
"type": "object"
-7
View File
@@ -4166,13 +4166,6 @@
"formatZH": "删除 FTP 账户 [users]",
"formatEN": "delete FTP users [users]"
},
"/toolbox/ftp/init": {
"bodyKeys": [],
"paramKeys": [],
"beforeFunctions": [],
"formatZH": "初始化 FTP 用户身份",
"formatEN": "initialize FTP user identity"
},
"/toolbox/ftp/operate": {
"bodyKeys": [
"operation"
-1
View File
@@ -85,7 +85,6 @@ export namespace Toolbox {
export interface FtpBaseInfo {
isActive: boolean;
isExist: boolean;
isInit: boolean;
}
export interface FtpInfo {
id: number;
-3
View File
@@ -76,9 +76,6 @@ export const updateFail2banByFile = (param: UpdateByFile) => {
export const getFtpBase = () => {
return http.get<Toolbox.FtpBaseInfo>(`/toolbox/ftp/base`);
};
export const initFtp = () => {
return http.post(`/toolbox/ftp/init`);
};
export const searchFtpLog = (param: Toolbox.FtpSearchLog) => {
return http.post<ResPage<Toolbox.FtpLog>>(`/toolbox/ftp/log/search`, param);
};
+27 -64
View File
@@ -8,34 +8,29 @@
<Status v-if="baseLoaded" class="mt-0.5" :status="form.isActive ? 'enable' : 'disable'" />
</div>
<div v-if="baseLoaded" class="mt-0.5">
<template v-if="form.isInit">
<el-button
v-if="form.isActive"
v-permission
v-node-admin
type="primary"
@click="onOperate('stop')"
link
>
{{ $t('commons.button.stop') }}
</el-button>
<el-button
v-if="!form.isActive"
v-permission
v-node-admin
type="primary"
@click="onOperate('start')"
link
>
{{ $t('commons.button.start') }}
</el-button>
<el-divider direction="vertical" />
<el-button v-permission v-node-admin type="primary" @click="onOperate('restart')" link>
{{ $t('commons.button.restart') }}
</el-button>
</template>
<el-button v-else v-permission v-node-admin type="primary" @click="onInit" link>
{{ $t('commons.button.init') }}
<el-button
v-if="form.isActive"
v-permission
v-node-admin
type="primary"
@click="onOperate('stop')"
link
>
{{ $t('commons.button.stop') }}
</el-button>
<el-button
v-if="!form.isActive"
v-permission
v-node-admin
type="primary"
@click="onOperate('start')"
link
>
{{ $t('commons.button.start') }}
</el-button>
<el-divider direction="vertical" />
<el-button v-permission v-node-admin type="primary" @click="onOperate('restart')" link>
{{ $t('commons.button.restart') }}
</el-button>
</div>
</div>
@@ -45,7 +40,7 @@
<LayoutContent
v-loading="loading"
:title="$t('toolbox.ftp.ftp', 2)"
:class="{ mask: baseLoaded && (!form.isActive || !form.isInit) }"
:class="{ mask: baseLoaded && !form.isActive }"
>
<template #prompt v-if="!isFxplay">
<el-alert type="info" :closable="false">
@@ -166,8 +161,8 @@
</template>
</LayoutContent>
<el-card v-if="baseLoaded && (!form.isActive || !form.isInit)" class="mask-prompt">
<span>{{ $t(form.isInit ? 'toolbox.ftp.notStart' : 'toolbox.ftp.initHelper') }}</span>
<el-card v-if="baseLoaded && !form.isActive" class="mask-prompt">
<span>{{ $t('toolbox.ftp.notStart') }}</span>
</el-card>
</div>
<NoSuchService v-if="!form.isExist" name="FTP (pure-ftpd)" />
@@ -182,7 +177,7 @@
import { onMounted, reactive, ref } from 'vue';
import i18n from '@/lang';
import { MsgError, MsgSuccess } from '@/utils/message';
import { deleteFtp, searchFtp, updateFtp, syncFtp, operateFtp, getFtpBase, initFtp } from '@/api/modules/toolbox';
import { deleteFtp, searchFtp, updateFtp, syncFtp, operateFtp, getFtpBase } from '@/api/modules/toolbox';
import OperateDialog from '@/views/toolbox/ftp/operate/index.vue';
import LogDialog from '@/views/toolbox/ftp/log/index.vue';
import { Toolbox } from '@/api/interface/toolbox';
@@ -209,7 +204,6 @@ const searchName = ref();
const form = reactive({
isActive: true,
isExist: true,
isInit: false,
});
const opRef = ref();
@@ -223,14 +217,7 @@ const search = async (column?: any) => {
.then(async (res) => {
form.isActive = res.data.isActive;
form.isExist = res.data.isExist;
form.isInit = res.data.isInit;
baseLoaded.value = true;
if (!form.isInit) {
loading.value = false;
data.value = [];
paginationConfig.total = 0;
return;
}
paginationConfig.orderBy = column?.order ? column.prop : paginationConfig.orderBy;
paginationConfig.order = column?.order ? column.order : paginationConfig.order;
let params = {
@@ -253,30 +240,6 @@ const search = async (column?: any) => {
});
};
const onInit = async () => {
ElMessageBox.confirm(
i18n.global.t('toolbox.ftp.operation', [i18n.global.t('commons.button.init')]),
i18n.global.t('commons.button.init'),
{
confirmButtonText: i18n.global.t('commons.button.confirm'),
cancelButtonText: i18n.global.t('commons.button.cancel'),
type: 'info',
},
)
.then(async () => {
loading.value = true;
await initFtp()
.then(() => {
MsgSuccess(i18n.global.t('commons.msg.operationSuccess'));
search();
})
.catch(() => {
loading.value = false;
});
})
.catch(() => {});
};
const onOperate = async (operation: string) => {
let msg = operation === 'enable' || operation === 'disable' ? 'ssh.' : 'commons.button.';
ElMessageBox.confirm(i18n.global.t('toolbox.ftp.operation', [i18n.global.t(msg + operation)]), 'FTP', {