mirror of https://github.com/1Panel-dev/1Panel
feat: 工具箱支持 FTP (#5039)
parent
35c4f4edfd
commit
64ed07e4fa
@ -0,0 +1,135 @@
|
|||||||
|
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/constant"
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
)
|
||||||
|
|
||||||
|
// @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)
|
||||||
|
}
|
@ -0,0 +1,30 @@
|
|||||||
|
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 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"`
|
||||||
|
}
|
@ -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"`
|
||||||
|
}
|
@ -0,0 +1,80 @@
|
|||||||
|
package repo
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/1Panel-dev/1Panel/backend/app/model"
|
||||||
|
"github.com/1Panel-dev/1Panel/backend/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
|
||||||
|
}
|
@ -0,0 +1,177 @@
|
|||||||
|
package service
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/1Panel-dev/1Panel/backend/app/dto"
|
||||||
|
"github.com/1Panel-dev/1Panel/backend/app/model"
|
||||||
|
"github.com/1Panel-dev/1Panel/backend/constant"
|
||||||
|
"github.com/1Panel-dev/1Panel/backend/utils/encrypt"
|
||||||
|
"github.com/1Panel-dev/1Panel/backend/utils/toolbox"
|
||||||
|
"github.com/jinzhu/copier"
|
||||||
|
"github.com/pkg/errors"
|
||||||
|
)
|
||||||
|
|
||||||
|
type FtpService struct{}
|
||||||
|
|
||||||
|
type IFtpService interface {
|
||||||
|
SearchWithPage(search dto.SearchWithPage) (int64, interface{}, error)
|
||||||
|
Create(req dto.FtpCreate) error
|
||||||
|
Delete(req dto.BatchDeleteReq) error
|
||||||
|
Update(req dto.FtpUpdate) error
|
||||||
|
Sync() error
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewIFtpService() IFtpService {
|
||||||
|
return &FtpService{}
|
||||||
|
}
|
||||||
|
|
||||||
|
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 {
|
||||||
|
_ = ftpRepo.Update(itemInDB.ID, map[string]interface{}{"path": item.Path, "status": constant.StatusDisable})
|
||||||
|
}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for _, item := range lists {
|
||||||
|
if _, ok := sameData[item.User]; !ok {
|
||||||
|
_ = ftpRepo.Create(&model.Ftp{User: item.User, Path: item.Path, Status: constant.StatusDisable})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
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) error {
|
||||||
|
pass, err := encrypt.StringEncrypt(req.Password)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
userInDB, _ := ftpRepo.Get(hostRepo.WithByUser(req.User))
|
||||||
|
if userInDB.ID != 0 {
|
||||||
|
return constant.ErrRecordExist
|
||||||
|
}
|
||||||
|
client, err := toolbox.NewFtpClient()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := client.UserAdd(req.User, req.Password, req.Path); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
var ftp model.Ftp
|
||||||
|
if err := copier.Copy(&ftp, &req); err != nil {
|
||||||
|
return errors.WithMessage(constant.ErrStructTransform, err.Error())
|
||||||
|
}
|
||||||
|
ftp.Status = constant.StatusEnable
|
||||||
|
ftp.Password = pass
|
||||||
|
if err := ftpRepo.Create(&ftp); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return 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 {
|
||||||
|
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
|
||||||
|
}
|
@ -0,0 +1,143 @@
|
|||||||
|
package toolbox
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"os/user"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/1Panel-dev/1Panel/backend/constant"
|
||||||
|
"github.com/1Panel-dev/1Panel/backend/utils/cmd"
|
||||||
|
"github.com/1Panel-dev/1Panel/backend/utils/systemctl"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Ftp struct {
|
||||||
|
DefaultUser string
|
||||||
|
}
|
||||||
|
|
||||||
|
type FtpClient interface {
|
||||||
|
Status() (bool, error)
|
||||||
|
LoadList() ([]FtpList, error)
|
||||||
|
UserAdd(username, path, passwd string) error
|
||||||
|
UserDel(username string) error
|
||||||
|
SetPasswd(username, passwd string) error
|
||||||
|
Reload() error
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewFtpClient() (*Ftp, error) {
|
||||||
|
userItem, err := user.LookupId("1000")
|
||||||
|
if err == nil {
|
||||||
|
return &Ftp{DefaultUser: userItem.Username}, 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"}, 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.Execf("useradd -u 1000 -g %s %s", groupItem.Name, userItem.Username)
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.New(stdout2)
|
||||||
|
}
|
||||||
|
return &Ftp{DefaultUser: "1panel"}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *Ftp) Status() (bool, error) {
|
||||||
|
return systemctl.IsActive("pure-ftpd.service")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *Ftp) UserAdd(username, passwd, path string) error {
|
||||||
|
std, err := cmd.Execf("pure-pw useradd %s -u %s -d %s <<EOF \n%s\n%s\nEOF", username, f.DefaultUser, path, passwd, passwd)
|
||||||
|
if err != nil {
|
||||||
|
return errors.New(std)
|
||||||
|
}
|
||||||
|
_ = f.Reload()
|
||||||
|
std2, err := cmd.Execf("chown %s %s", f.DefaultUser, path)
|
||||||
|
if err != nil {
|
||||||
|
return errors.New(std2)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *Ftp) UserDel(username string) error {
|
||||||
|
std, err := cmd.Execf("pure-pw userdel %s", username)
|
||||||
|
if err != nil {
|
||||||
|
return errors.New(std)
|
||||||
|
}
|
||||||
|
_ = f.Reload()
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *Ftp) SetPasswd(username, passwd string) error {
|
||||||
|
std, err := cmd.Execf("pure-pw passwd %s <<EOF \n%s\n%s\nEOF", username, passwd, passwd)
|
||||||
|
if err != nil {
|
||||||
|
return errors.New(std)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *Ftp) SetPath(username, path string) error {
|
||||||
|
std, err := cmd.Execf("pure-pw usermod %s -d %s", username, path)
|
||||||
|
if err != nil {
|
||||||
|
return errors.New(std)
|
||||||
|
}
|
||||||
|
std2, err := cmd.Execf("chown %s %s", f.DefaultUser, path)
|
||||||
|
if err != nil {
|
||||||
|
return errors.New(std2)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *Ftp) SetStatus(username, status string) error {
|
||||||
|
statusItem := "''"
|
||||||
|
if status == constant.StatusDisable {
|
||||||
|
statusItem = "1"
|
||||||
|
}
|
||||||
|
std, err := cmd.Execf("pure-pw usermod %s -r %s", username, statusItem)
|
||||||
|
if err != nil {
|
||||||
|
return errors.New(std)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *Ftp) LoadList() ([]FtpList, error) {
|
||||||
|
std, err := cmd.Exec("pure-pw list")
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.New(std)
|
||||||
|
}
|
||||||
|
var lists []FtpList
|
||||||
|
lines := strings.Split(std, "\n")
|
||||||
|
for _, line := range lines {
|
||||||
|
parts := strings.Fields(line)
|
||||||
|
if len(parts) < 2 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
lists = append(lists, FtpList{User: parts[0], Path: strings.ReplaceAll(parts[1], "/./", "")})
|
||||||
|
}
|
||||||
|
return lists, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type FtpList struct {
|
||||||
|
User string
|
||||||
|
Path string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *Ftp) Reload() error {
|
||||||
|
std, err := cmd.Exec("pure-pw mkdb")
|
||||||
|
if err != nil {
|
||||||
|
return errors.New(std)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
@ -0,0 +1,268 @@
|
|||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<LayoutContent v-loading="loading" title="FTP">
|
||||||
|
<template #toolbar>
|
||||||
|
<el-row>
|
||||||
|
<el-col :xs="24" :sm="16" :md="16" :lg="16" :xl="16">
|
||||||
|
<el-button type="primary" @click="onOpenDialog('add')">
|
||||||
|
{{ $t('commons.button.add') }} FTP
|
||||||
|
</el-button>
|
||||||
|
<el-button @click="onSync()">
|
||||||
|
{{ $t('commons.button.sync') }}
|
||||||
|
</el-button>
|
||||||
|
<el-button plain :disabled="selects.length === 0" @click="onDelete(null)">
|
||||||
|
{{ $t('commons.button.delete') }}
|
||||||
|
</el-button>
|
||||||
|
</el-col>
|
||||||
|
<el-col :xs="24" :sm="8" :md="8" :lg="8" :xl="8">
|
||||||
|
<TableSearch @search="search()" v-model:searchName="searchName" />
|
||||||
|
</el-col>
|
||||||
|
</el-row>
|
||||||
|
</template>
|
||||||
|
<template #main>
|
||||||
|
<ComplexTable
|
||||||
|
:pagination-config="paginationConfig"
|
||||||
|
v-model:selects="selects"
|
||||||
|
@sort-change="search"
|
||||||
|
@search="search"
|
||||||
|
:data="data"
|
||||||
|
>
|
||||||
|
<el-table-column type="selection" fix />
|
||||||
|
<el-table-column
|
||||||
|
:label="$t('commons.login.username')"
|
||||||
|
:min-width="60"
|
||||||
|
prop="user"
|
||||||
|
show-overflow-tooltip
|
||||||
|
/>
|
||||||
|
<el-table-column :label="$t('commons.login.password')" prop="password">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<div v-if="row.password.length === 0">-</div>
|
||||||
|
<div v-else class="flex items-center">
|
||||||
|
<div class="star-center" v-if="!row.showPassword">
|
||||||
|
<span>**********</span>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<span v-if="row.showPassword">
|
||||||
|
{{ row.password }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<el-button
|
||||||
|
v-if="!row.showPassword"
|
||||||
|
link
|
||||||
|
@click="row.showPassword = true"
|
||||||
|
icon="View"
|
||||||
|
class="ml-1.5"
|
||||||
|
></el-button>
|
||||||
|
<el-button
|
||||||
|
v-if="row.showPassword"
|
||||||
|
link
|
||||||
|
@click="row.showPassword = false"
|
||||||
|
icon="Hide"
|
||||||
|
class="ml-1.5"
|
||||||
|
></el-button>
|
||||||
|
<div>
|
||||||
|
<CopyButton :content="row.password" type="icon" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column :label="$t('commons.table.status')" :min-width="60" prop="status">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<el-tag v-if="row.status === 'deleted'" type="info">{{ $t('database.isDelete') }}</el-tag>
|
||||||
|
<el-button
|
||||||
|
v-if="row.status === 'Enable'"
|
||||||
|
@click="onChangeStatus(row, 'disable')"
|
||||||
|
link
|
||||||
|
icon="VideoPlay"
|
||||||
|
type="success"
|
||||||
|
>
|
||||||
|
{{ $t('commons.status.enabled') }}
|
||||||
|
</el-button>
|
||||||
|
<el-button
|
||||||
|
v-if="row.status === 'Disable'"
|
||||||
|
icon="VideoPause"
|
||||||
|
@click="onChangeStatus(row, 'enable')"
|
||||||
|
link
|
||||||
|
type="danger"
|
||||||
|
>
|
||||||
|
{{ $t('commons.status.disabled') }}
|
||||||
|
</el-button>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column :label="$t('file.root')" :min-width="120" prop="path" show-overflow-tooltip />
|
||||||
|
<el-table-column
|
||||||
|
:label="$t('commons.table.description')"
|
||||||
|
:min-width="80"
|
||||||
|
prop="description"
|
||||||
|
show-overflow-tooltip
|
||||||
|
/>
|
||||||
|
<el-table-column
|
||||||
|
:label="$t('commons.table.createdAt')"
|
||||||
|
:formatter="dateFormat"
|
||||||
|
:min-width="80"
|
||||||
|
prop="createdAt"
|
||||||
|
/>
|
||||||
|
<fu-table-operations
|
||||||
|
width="240px"
|
||||||
|
:buttons="buttons"
|
||||||
|
:ellipsis="10"
|
||||||
|
:label="$t('commons.table.operate')"
|
||||||
|
fix
|
||||||
|
/>
|
||||||
|
</ComplexTable>
|
||||||
|
</template>
|
||||||
|
</LayoutContent>
|
||||||
|
|
||||||
|
<OpDialog ref="opRef" @search="search" @submit="onSubmitDelete()" />
|
||||||
|
<OperateDialog @search="search" ref="dialogRef" />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { onMounted, reactive, ref } from 'vue';
|
||||||
|
import i18n from '@/lang';
|
||||||
|
import { dateFormat } from '@/utils/util';
|
||||||
|
import { MsgSuccess } from '@/utils/message';
|
||||||
|
import { deleteFtp, searchFtp, updateFtp, syncFtp } from '@/api/modules/toolbox';
|
||||||
|
import OperateDialog from '@/views/toolbox/ftp/operate/index.vue';
|
||||||
|
import { Toolbox } from '@/api/interface/toolbox';
|
||||||
|
|
||||||
|
const loading = ref();
|
||||||
|
const selects = ref<any>([]);
|
||||||
|
|
||||||
|
const data = ref();
|
||||||
|
const paginationConfig = reactive({
|
||||||
|
cacheSizeKey: 'ftp-page-size',
|
||||||
|
currentPage: 1,
|
||||||
|
pageSize: 10,
|
||||||
|
total: 0,
|
||||||
|
orderBy: 'created_at',
|
||||||
|
order: 'null',
|
||||||
|
});
|
||||||
|
const searchName = ref();
|
||||||
|
|
||||||
|
const opRef = ref();
|
||||||
|
const dialogRef = ref();
|
||||||
|
const operateIDs = ref();
|
||||||
|
|
||||||
|
const search = async (column?: any) => {
|
||||||
|
paginationConfig.orderBy = column?.order ? column.prop : paginationConfig.orderBy;
|
||||||
|
paginationConfig.order = column?.order ? column.order : paginationConfig.order;
|
||||||
|
let params = {
|
||||||
|
info: searchName.value,
|
||||||
|
page: paginationConfig.currentPage,
|
||||||
|
pageSize: paginationConfig.pageSize,
|
||||||
|
};
|
||||||
|
loading.value = true;
|
||||||
|
await searchFtp(params)
|
||||||
|
.then((res) => {
|
||||||
|
loading.value = false;
|
||||||
|
data.value = res.data.items || [];
|
||||||
|
paginationConfig.total = res.data.total;
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
loading.value = false;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const onChangeStatus = async (row: Toolbox.FtpInfo, status: string) => {
|
||||||
|
ElMessageBox.confirm(i18n.global.t('toolbox.ftp.' + status + 'Helper'), i18n.global.t('cronjob.changeStatus'), {
|
||||||
|
confirmButtonText: i18n.global.t('commons.button.confirm'),
|
||||||
|
cancelButtonText: i18n.global.t('commons.button.cancel'),
|
||||||
|
}).then(async () => {
|
||||||
|
row.status = status === 'enable' ? 'Enable' : 'Disable';
|
||||||
|
await updateFtp(row);
|
||||||
|
MsgSuccess(i18n.global.t('commons.msg.operationSuccess'));
|
||||||
|
search();
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const onOpenDialog = async (title: string, rowData: Partial<Toolbox.FtpInfo> = {}) => {
|
||||||
|
let params = {
|
||||||
|
title,
|
||||||
|
rowData: { ...rowData },
|
||||||
|
};
|
||||||
|
dialogRef.value!.acceptParams(params);
|
||||||
|
};
|
||||||
|
|
||||||
|
const onSync = async () => {
|
||||||
|
ElMessageBox.confirm(i18n.global.t('toolbox.ftp.syncHelper'), i18n.global.t('commons.button.sync'), {
|
||||||
|
confirmButtonText: i18n.global.t('commons.button.confirm'),
|
||||||
|
cancelButtonText: i18n.global.t('commons.button.cancel'),
|
||||||
|
}).then(async () => {
|
||||||
|
loading.value = true;
|
||||||
|
await syncFtp()
|
||||||
|
.then(() => {
|
||||||
|
loading.value = false;
|
||||||
|
MsgSuccess(i18n.global.t('toolbox.ftp.operationSuccess'));
|
||||||
|
search();
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
loading.value = false;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const onDelete = async (row: Toolbox.FtpInfo | null) => {
|
||||||
|
let names = [];
|
||||||
|
let ids = [];
|
||||||
|
if (row) {
|
||||||
|
ids = [row.id];
|
||||||
|
names = [row.user];
|
||||||
|
} else {
|
||||||
|
for (const item of selects.value) {
|
||||||
|
names.push(item.user);
|
||||||
|
ids.push(item.id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
operateIDs.value = ids;
|
||||||
|
opRef.value.acceptParams({
|
||||||
|
title: i18n.global.t('commons.button.delete'),
|
||||||
|
names: names,
|
||||||
|
msg: i18n.global.t('commons.msg.operatorHelper', [
|
||||||
|
i18n.global.t('cronjob.cronTask'),
|
||||||
|
i18n.global.t('commons.button.delete'),
|
||||||
|
]),
|
||||||
|
api: null,
|
||||||
|
params: null,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const onSubmitDelete = async () => {
|
||||||
|
loading.value = true;
|
||||||
|
await deleteFtp({ ids: operateIDs.value })
|
||||||
|
.then(() => {
|
||||||
|
loading.value = false;
|
||||||
|
MsgSuccess(i18n.global.t('commons.msg.deleteSuccess'));
|
||||||
|
search();
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
loading.value = false;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const buttons = [
|
||||||
|
{
|
||||||
|
label: i18n.global.t('commons.button.edit'),
|
||||||
|
disabled: (row: Toolbox.FtpInfo) => {
|
||||||
|
return row.status === 'deleted';
|
||||||
|
},
|
||||||
|
click: (row: Toolbox.FtpInfo) => {
|
||||||
|
onOpenDialog('edit', row);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: i18n.global.t('commons.button.delete'),
|
||||||
|
disabled: (row: Toolbox.FtpInfo) => {
|
||||||
|
return row.status === 'deleted';
|
||||||
|
},
|
||||||
|
click: (row: Toolbox.FtpInfo) => {
|
||||||
|
onDelete(row);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
search();
|
||||||
|
});
|
||||||
|
</script>
|
@ -0,0 +1,137 @@
|
|||||||
|
<template>
|
||||||
|
<el-drawer
|
||||||
|
v-model="drawerVisible"
|
||||||
|
:destroy-on-close="true"
|
||||||
|
:close-on-click-modal="false"
|
||||||
|
:close-on-press-escape="false"
|
||||||
|
size="50%"
|
||||||
|
>
|
||||||
|
<template #header>
|
||||||
|
<DrawerHeader
|
||||||
|
:header="title"
|
||||||
|
:hideResource="dialogData.title === 'add'"
|
||||||
|
:resource="dialogData.rowData?.user"
|
||||||
|
:back="handleClose"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
<el-form ref="formRef" label-position="top" :model="dialogData.rowData" :rules="rules">
|
||||||
|
<el-row type="flex" justify="center">
|
||||||
|
<el-col :span="22">
|
||||||
|
<el-form-item :label="$t('commons.login.username')" prop="user">
|
||||||
|
<el-input
|
||||||
|
:disabled="dialogData.title === 'edit'"
|
||||||
|
clearable
|
||||||
|
v-model.trim="dialogData.rowData!.user"
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item :label="$t('commons.login.password')" prop="password">
|
||||||
|
<el-input clearable v-model="dialogData.rowData!.password" />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item :label="$t('file.root')" prop="path">
|
||||||
|
<el-input v-model="dialogData.rowData!.path">
|
||||||
|
<template #prepend>
|
||||||
|
<FileList @choose="loadDir" :dir="true"></FileList>
|
||||||
|
</template>
|
||||||
|
</el-input>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item :label="$t('commons.table.description')" prop="description">
|
||||||
|
<el-input type="textarea" :rows="3" clearable v-model="dialogData.rowData!.description" />
|
||||||
|
</el-form-item>
|
||||||
|
</el-col>
|
||||||
|
</el-row>
|
||||||
|
</el-form>
|
||||||
|
<template #footer>
|
||||||
|
<span class="dialog-footer">
|
||||||
|
<el-button @click="drawerVisible = false">{{ $t('commons.button.cancel') }}</el-button>
|
||||||
|
<el-button type="primary" @click="onSubmit(formRef)">
|
||||||
|
{{ $t('commons.button.confirm') }}
|
||||||
|
</el-button>
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
|
</el-drawer>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { reactive, ref } from 'vue';
|
||||||
|
import { Rules } from '@/global/form-rules';
|
||||||
|
import FileList from '@/components/file-list/index.vue';
|
||||||
|
import i18n from '@/lang';
|
||||||
|
import { ElForm } from 'element-plus';
|
||||||
|
import DrawerHeader from '@/components/drawer-header/index.vue';
|
||||||
|
import { MsgSuccess } from '@/utils/message';
|
||||||
|
import { Toolbox } from '@/api/interface/toolbox';
|
||||||
|
import { createFtp, updateFtp } from '@/api/modules/toolbox';
|
||||||
|
|
||||||
|
interface DialogProps {
|
||||||
|
title: string;
|
||||||
|
rowData?: Toolbox.FtpInfo;
|
||||||
|
getTableList?: () => Promise<any>;
|
||||||
|
}
|
||||||
|
const loading = ref();
|
||||||
|
const title = ref<string>('');
|
||||||
|
const drawerVisible = ref(false);
|
||||||
|
const dialogData = ref<DialogProps>({
|
||||||
|
title: '',
|
||||||
|
});
|
||||||
|
|
||||||
|
const acceptParams = (params: DialogProps): void => {
|
||||||
|
dialogData.value = params;
|
||||||
|
title.value = i18n.global.t('commons.button.' + dialogData.value.title);
|
||||||
|
drawerVisible.value = true;
|
||||||
|
};
|
||||||
|
const emit = defineEmits<{ (e: 'search'): void }>();
|
||||||
|
|
||||||
|
const handleClose = () => {
|
||||||
|
drawerVisible.value = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
const rules = reactive({
|
||||||
|
user: [Rules.requiredInput, Rules.noSpace],
|
||||||
|
password: [Rules.requiredInput],
|
||||||
|
path: [Rules.requiredInput],
|
||||||
|
});
|
||||||
|
|
||||||
|
type FormInstance = InstanceType<typeof ElForm>;
|
||||||
|
const formRef = ref<FormInstance>();
|
||||||
|
|
||||||
|
const loadDir = async (path: string) => {
|
||||||
|
dialogData.value.rowData!.path = path;
|
||||||
|
};
|
||||||
|
|
||||||
|
const onSubmit = async (formEl: FormInstance | undefined) => {
|
||||||
|
if (!formEl) return;
|
||||||
|
formEl.validate(async (valid) => {
|
||||||
|
if (!valid) return;
|
||||||
|
if (dialogData.value.title === 'edit') {
|
||||||
|
loading.value = true;
|
||||||
|
await updateFtp(dialogData.value.rowData)
|
||||||
|
.then(() => {
|
||||||
|
loading.value = false;
|
||||||
|
drawerVisible.value = false;
|
||||||
|
MsgSuccess(i18n.global.t('commons.msg.operationSuccess'));
|
||||||
|
emit('search');
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
loading.value = false;
|
||||||
|
});
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await createFtp(dialogData.value.rowData)
|
||||||
|
.then(() => {
|
||||||
|
loading.value = false;
|
||||||
|
MsgSuccess(i18n.global.t('commons.msg.operationSuccess'));
|
||||||
|
emit('search');
|
||||||
|
drawerVisible.value = false;
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
loading.value = false;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
defineExpose({
|
||||||
|
acceptParams,
|
||||||
|
});
|
||||||
|
</script>
|
Loading…
Reference in new issue