feat: FTP 增加状态及日志 (#5065)

This commit is contained in:
ssongliu 2024-05-20 18:48:42 +08:00 committed by GitHub
parent 201600ea06
commit 65f92bf0c3
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
17 changed files with 953 additions and 122 deletions

View File

@ -9,6 +9,70 @@ import (
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
) )
// @Tags FTP
// @Summary Load FTP base info
// @Description 获取 FTP 基础信息
// @Success 200 {object} dto.FtpBaseInfo
// @Security ApiKeyAuth
// @Router /toolbox/ftp/base [get]
func (b *BaseApi) LoadFtpBaseInfo(c *gin.Context) {
data, err := ftpService.LoadBaseInfo()
if err != nil {
helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err)
return
}
helper.SuccessWithData(c, data)
}
// @Tags FTP
// @Summary Load FTP operation log
// @Description 获取 FTP 操作日志
// @Accept json
// @Param request body dto.FtpLogSearch true "request"
// @Success 200 {object} dto.PageResult
// @Security ApiKeyAuth
// @Router /toolbox/ftp/log/search [post]
func (b *BaseApi) LoadFtpLogInfo(c *gin.Context) {
var req dto.FtpLogSearch
if err := helper.CheckBindAndValidate(&req, c); err != nil {
return
}
total, list, err := ftpService.LoadLog(req)
if err != nil {
helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err)
return
}
helper.SuccessWithData(c, dto.PageResult{
Items: list,
Total: total,
})
}
// @Tags FTP
// @Summary Operate FTP
// @Description 修改 FTP 状态
// @Accept json
// @Param request body dto.Operate true "request"
// @Security ApiKeyAuth
// @Router /toolbox/ftp/operate [post]
// @x-panel-log {"bodyKeys":["operation"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"[operation] FTP","formatEN":"[operation] FTP"}
func (b *BaseApi) OperateFtp(c *gin.Context) {
var req dto.Operate
if err := helper.CheckBindAndValidate(&req, c); err != nil {
return
}
if err := ftpService.Operate(req.Operation); err != nil {
helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err)
return
}
helper.SuccessWithData(c, nil)
}
// @Tags FTP // @Tags FTP
// @Summary Page FTP user // @Summary Page FTP user
// @Description 获取 FTP 账户列表分页 // @Description 获取 FTP 账户列表分页

View File

@ -1,6 +1,8 @@
package dto package dto
import "time" import (
"time"
)
type FtpInfo struct { type FtpInfo struct {
ID uint `json:"id"` ID uint `json:"id"`
@ -13,6 +15,17 @@ type FtpInfo struct {
Description string `json:"description"` Description string `json:"description"`
} }
type FtpBaseInfo struct {
IsActive bool `json:"isActive"`
IsExist bool `json:"isExist"`
}
type FtpLogSearch struct {
PageInfo
User string `json:"user"`
Operation string `json:"operation"`
}
type FtpCreate struct { type FtpCreate struct {
User string `json:"user" validate:"required"` User string `json:"user" validate:"required"`
Password string `json:"password" validate:"required"` Password string `json:"password" validate:"required"`

View File

@ -13,17 +13,60 @@ import (
type FtpService struct{} type FtpService struct{}
type IFtpService interface { type IFtpService interface {
LoadBaseInfo() (dto.FtpBaseInfo, error)
SearchWithPage(search dto.SearchWithPage) (int64, interface{}, error) SearchWithPage(search dto.SearchWithPage) (int64, interface{}, error)
Operate(operation string) error
Create(req dto.FtpCreate) error Create(req dto.FtpCreate) error
Delete(req dto.BatchDeleteReq) error Delete(req dto.BatchDeleteReq) error
Update(req dto.FtpUpdate) error Update(req dto.FtpUpdate) error
Sync() error Sync() error
LoadLog(req dto.FtpLogSearch) (int64, interface{}, error)
} }
func NewIFtpService() IFtpService { func NewIFtpService() IFtpService {
return &FtpService{} return &FtpService{}
} }
func (f *FtpService) LoadBaseInfo() (dto.FtpBaseInfo, error) {
var baseInfo dto.FtpBaseInfo
client, err := toolbox.NewFtpClient()
if err != nil {
return baseInfo, err
}
baseInfo.IsActive, baseInfo.IsExist = client.Status()
return baseInfo, nil
}
func (f *FtpService) LoadLog(req dto.FtpLogSearch) (int64, interface{}, error) {
client, err := toolbox.NewFtpClient()
if err != nil {
return 0, nil, err
}
logItem, err := client.LoadLogs(req.User, req.Operation)
if err != nil {
return 0, nil, err
}
var logs []toolbox.FtpLog
total, start, end := len(logItem), (req.Page-1)*req.PageSize, req.Page*req.PageSize
if start > total {
logs = make([]toolbox.FtpLog, 0)
} else {
if end >= total {
end = total
}
logs = logItem[start:end]
}
return int64(total), logs, nil
}
func (u *FtpService) Operate(operation string) error {
client, err := toolbox.NewFtpClient()
if err != nil {
return err
}
return client.Operate(operation)
}
func (f *FtpService) SearchWithPage(req dto.SearchWithPage) (int64, interface{}, error) { 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")) total, lists, err := ftpRepo.Page(req.Page, req.PageSize, ftpRepo.WithByUser(req.Info), commonRepo.WithOrderBy("created_at desc"))
if err != nil { if err != nil {

View File

@ -37,6 +37,9 @@ func (s *ToolboxRouter) InitRouter(Router *gin.RouterGroup) {
toolboxRouter.POST("/fail2ban/update", baseApi.UpdateFail2BanConf) toolboxRouter.POST("/fail2ban/update", baseApi.UpdateFail2BanConf)
toolboxRouter.POST("/fail2ban/update/byconf", baseApi.UpdateFail2BanConfByFile) toolboxRouter.POST("/fail2ban/update/byconf", baseApi.UpdateFail2BanConfByFile)
toolboxRouter.GET("/ftp/base", baseApi.LoadFtpBaseInfo)
toolboxRouter.POST("/ftp/log/search", baseApi.LoadFtpLogInfo)
toolboxRouter.POST("/ftp/operate", baseApi.OperateFtp)
toolboxRouter.POST("/ftp/search", baseApi.SearchFtp) toolboxRouter.POST("/ftp/search", baseApi.SearchFtp)
toolboxRouter.POST("/ftp", baseApi.CreateFtp) toolboxRouter.POST("/ftp", baseApi.CreateFtp)
toolboxRouter.POST("/ftp/update", baseApi.UpdateFtp) toolboxRouter.POST("/ftp/update", baseApi.UpdateFtp)

View File

@ -15,7 +15,7 @@ type Fail2ban struct{}
const defaultPath = "/etc/fail2ban/jail.local" const defaultPath = "/etc/fail2ban/jail.local"
type FirewallClient interface { type FirewallClient interface {
Status() (bool, bool, bool, error) Status() (bool, bool, bool)
Version() (string, error) Version() (string, error)
Operate(operate string) error Operate(operate string) error
OperateSSHD(operate, ip string) error OperateSSHD(operate, ip string) error

View File

@ -2,8 +2,13 @@ package toolbox
import ( import (
"errors" "errors"
"fmt"
"os"
"os/user" "os/user"
"path"
"path/filepath"
"strings" "strings"
"time"
"github.com/1Panel-dev/1Panel/backend/constant" "github.com/1Panel-dev/1Panel/backend/constant"
"github.com/1Panel-dev/1Panel/backend/utils/cmd" "github.com/1Panel-dev/1Panel/backend/utils/cmd"
@ -15,12 +20,14 @@ type Ftp struct {
} }
type FtpClient interface { type FtpClient interface {
Status() (bool, error) Status() (bool, bool)
Operate(operate string) error
LoadList() ([]FtpList, error) LoadList() ([]FtpList, error)
UserAdd(username, path, passwd string) error UserAdd(username, path, passwd string) error
UserDel(username string) error UserDel(username string) error
SetPasswd(username, passwd string) error SetPasswd(username, passwd string) error
Reload() error Reload() error
LoadLogs() ([]FtpLog, error)
} }
func NewFtpClient() (*Ftp, error) { func NewFtpClient() (*Ftp, error) {
@ -54,8 +61,24 @@ func NewFtpClient() (*Ftp, error) {
return &Ftp{DefaultUser: "1panel"}, nil return &Ftp{DefaultUser: "1panel"}, nil
} }
func (f *Ftp) Status() (bool, error) { func (f *Ftp) Status() (bool, bool) {
return systemctl.IsActive("pure-ftpd.service") isActive, _ := systemctl.IsActive("pure-ftpd.service")
isExist, _ := systemctl.IsExist("pure-ftpd.service")
return isActive, isExist
}
func (f *Ftp) Operate(operate string) error {
switch operate {
case "start", "restart", "stop":
stdout, err := cmd.Execf("systemctl %s pure-ftpd.service", operate)
if err != nil {
return fmt.Errorf("%s the pure-ftpd.service failed, err: %s", operate, stdout)
}
return nil
default:
return fmt.Errorf("not support such operation: %v", operate)
}
} }
func (f *Ftp) UserAdd(username, passwd, path string) error { func (f *Ftp) UserAdd(username, passwd, path string) error {
@ -141,3 +164,92 @@ func (f *Ftp) Reload() error {
} }
return nil return nil
} }
func (f *Ftp) LoadLogs(user, operation string) ([]FtpLog, error) {
var logs []FtpLog
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 = 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 = std
}
logItem = strings.ReplaceAll(logItem, "AltLog", "")
logItem = strings.ReplaceAll(logItem, "clf:", "")
logItem = strings.ReplaceAll(logItem, "\n", "")
logPath := strings.Trim(logItem, " ")
fileName := path.Base(logPath)
var fileList []string
if err := filepath.Walk(path.Dir(logPath), func(pathItem string, info os.FileInfo, err error) error {
if err != nil {
return err
}
if !info.IsDir() && strings.HasPrefix(info.Name(), fileName) {
fileList = append(fileList, pathItem)
}
return nil
}); err != nil {
return nil, err
}
logs = loadLogsByFiles(fileList, user, operation)
return logs, nil
}
func loadLogsByFiles(fileList []string, user, operation string) []FtpLog {
var logs []FtpLog
layout := "02/Jan/2006:15:04:05-0700"
for _, file := range fileList {
data, err := os.ReadFile(file)
if err != nil {
continue
}
lines := strings.Split(string(data), "\n")
for _, line := range lines {
parts := strings.Fields(line)
if len(parts) < 9 {
continue
}
if (len(user) != 0 && parts[2] != user) || (len(operation) != 0 && parts[5] != fmt.Sprintf("\"%s", operation)) {
continue
}
timeStr := parts[3] + parts[4]
timeStr = strings.ReplaceAll(timeStr, "[", "")
timeStr = strings.ReplaceAll(timeStr, "]", "")
timeItem, err := time.Parse(layout, timeStr)
if err == nil {
timeStr = timeItem.Format("2006-01-02 15:04:05")
}
operateStr := parts[5] + parts[6]
logs = append(logs, FtpLog{
IP: parts[0],
User: parts[2],
Time: timeStr,
Operation: operateStr,
Status: parts[7],
Size: parts[8],
})
}
}
return logs
}
type FtpLog struct {
IP string `json:"ip"`
User string `json:"user"`
Time string `json:"time"`
Operation string `json:"operation"`
Status string `json:"status"`
Size string `json:"size"`
}

View File

@ -11555,6 +11555,28 @@ const docTemplate = `{
} }
} }
}, },
"/toolbox/ftp/base": {
"get": {
"security": [
{
"ApiKeyAuth": []
}
],
"description": "获取 FTP 基础信息",
"tags": [
"FTP"
],
"summary": "Load FTP base info",
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/dto.FtpBaseInfo"
}
}
}
}
},
"/toolbox/ftp/del": { "/toolbox/ftp/del": {
"post": { "post": {
"security": [ "security": [
@ -11606,6 +11628,80 @@ const docTemplate = `{
} }
} }
}, },
"/toolbox/ftp/log/search": {
"post": {
"security": [
{
"ApiKeyAuth": []
}
],
"description": "获取 FTP 操作日志",
"consumes": [
"application/json"
],
"tags": [
"FTP"
],
"summary": "Load FTP operation log",
"parameters": [
{
"description": "request",
"name": "request",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/dto.FtpLogSearch"
}
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/dto.PageResult"
}
}
}
}
},
"/toolbox/ftp/operate": {
"post": {
"security": [
{
"ApiKeyAuth": []
}
],
"description": "修改 FTP 状态",
"consumes": [
"application/json"
],
"tags": [
"FTP"
],
"summary": "Operate FTP",
"parameters": [
{
"description": "request",
"name": "request",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/dto.Operate"
}
}
],
"responses": {},
"x-panel-log": {
"BeforeFunctions": [],
"bodyKeys": [
"operation"
],
"formatEN": "[operation] FTP",
"formatZH": "[operation] FTP",
"paramKeys": []
}
}
},
"/toolbox/ftp/search": { "/toolbox/ftp/search": {
"post": { "post": {
"security": [ "security": [
@ -16182,6 +16278,17 @@ const docTemplate = `{
} }
} }
}, },
"dto.FtpBaseInfo": {
"type": "object",
"properties": {
"isActive": {
"type": "boolean"
},
"isExist": {
"type": "boolean"
}
}
},
"dto.FtpCreate": { "dto.FtpCreate": {
"type": "object", "type": "object",
"required": [ "required": [
@ -16204,6 +16311,27 @@ const docTemplate = `{
} }
} }
}, },
"dto.FtpLogSearch": {
"type": "object",
"required": [
"page",
"pageSize"
],
"properties": {
"operation": {
"type": "string"
},
"page": {
"type": "integer"
},
"pageSize": {
"type": "integer"
},
"user": {
"type": "string"
}
}
},
"dto.FtpUpdate": { "dto.FtpUpdate": {
"type": "object", "type": "object",
"required": [ "required": [

View File

@ -11548,6 +11548,28 @@
} }
} }
}, },
"/toolbox/ftp/base": {
"get": {
"security": [
{
"ApiKeyAuth": []
}
],
"description": "获取 FTP 基础信息",
"tags": [
"FTP"
],
"summary": "Load FTP base info",
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/dto.FtpBaseInfo"
}
}
}
}
},
"/toolbox/ftp/del": { "/toolbox/ftp/del": {
"post": { "post": {
"security": [ "security": [
@ -11599,6 +11621,80 @@
} }
} }
}, },
"/toolbox/ftp/log/search": {
"post": {
"security": [
{
"ApiKeyAuth": []
}
],
"description": "获取 FTP 操作日志",
"consumes": [
"application/json"
],
"tags": [
"FTP"
],
"summary": "Load FTP operation log",
"parameters": [
{
"description": "request",
"name": "request",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/dto.FtpLogSearch"
}
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/dto.PageResult"
}
}
}
}
},
"/toolbox/ftp/operate": {
"post": {
"security": [
{
"ApiKeyAuth": []
}
],
"description": "修改 FTP 状态",
"consumes": [
"application/json"
],
"tags": [
"FTP"
],
"summary": "Operate FTP",
"parameters": [
{
"description": "request",
"name": "request",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/dto.Operate"
}
}
],
"responses": {},
"x-panel-log": {
"BeforeFunctions": [],
"bodyKeys": [
"operation"
],
"formatEN": "[operation] FTP",
"formatZH": "[operation] FTP",
"paramKeys": []
}
}
},
"/toolbox/ftp/search": { "/toolbox/ftp/search": {
"post": { "post": {
"security": [ "security": [
@ -16175,6 +16271,17 @@
} }
} }
}, },
"dto.FtpBaseInfo": {
"type": "object",
"properties": {
"isActive": {
"type": "boolean"
},
"isExist": {
"type": "boolean"
}
}
},
"dto.FtpCreate": { "dto.FtpCreate": {
"type": "object", "type": "object",
"required": [ "required": [
@ -16197,6 +16304,27 @@
} }
} }
}, },
"dto.FtpLogSearch": {
"type": "object",
"required": [
"page",
"pageSize"
],
"properties": {
"operation": {
"type": "string"
},
"page": {
"type": "integer"
},
"pageSize": {
"type": "integer"
},
"user": {
"type": "string"
}
}
},
"dto.FtpUpdate": { "dto.FtpUpdate": {
"type": "object", "type": "object",
"required": [ "required": [

View File

@ -1202,6 +1202,13 @@ definitions:
- type - type
- vars - vars
type: object type: object
dto.FtpBaseInfo:
properties:
isActive:
type: boolean
isExist:
type: boolean
type: object
dto.FtpCreate: dto.FtpCreate:
properties: properties:
description: description:
@ -1217,6 +1224,20 @@ definitions:
- path - path
- user - user
type: object type: object
dto.FtpLogSearch:
properties:
operation:
type: string
page:
type: integer
pageSize:
type: integer
user:
type: string
required:
- page
- pageSize
type: object
dto.FtpUpdate: dto.FtpUpdate:
properties: properties:
description: description:
@ -12456,6 +12477,19 @@ paths:
formatEN: create FTP [user][path] formatEN: create FTP [user][path]
formatZH: 创建 FTP 账户 [user][path] formatZH: 创建 FTP 账户 [user][path]
paramKeys: [] paramKeys: []
/toolbox/ftp/base:
get:
description: 获取 FTP 基础信息
responses:
"200":
description: OK
schema:
$ref: '#/definitions/dto.FtpBaseInfo'
security:
- ApiKeyAuth: []
summary: Load FTP base info
tags:
- FTP
/toolbox/ftp/del: /toolbox/ftp/del:
post: post:
consumes: consumes:
@ -12489,6 +12523,53 @@ paths:
formatEN: delete FTP users [users] formatEN: delete FTP users [users]
formatZH: 删除 FTP 账户 [users] formatZH: 删除 FTP 账户 [users]
paramKeys: [] paramKeys: []
/toolbox/ftp/log/search:
post:
consumes:
- application/json
description: 获取 FTP 操作日志
parameters:
- description: request
in: body
name: request
required: true
schema:
$ref: '#/definitions/dto.FtpLogSearch'
responses:
"200":
description: OK
schema:
$ref: '#/definitions/dto.PageResult'
security:
- ApiKeyAuth: []
summary: Load FTP operation log
tags:
- FTP
/toolbox/ftp/operate:
post:
consumes:
- application/json
description: 修改 FTP 状态
parameters:
- description: request
in: body
name: request
required: true
schema:
$ref: '#/definitions/dto.Operate'
responses: {}
security:
- ApiKeyAuth: []
summary: Operate FTP
tags:
- FTP
x-panel-log:
BeforeFunctions: []
bodyKeys:
- operation
formatEN: '[operation] FTP'
formatZH: '[operation] FTP'
paramKeys: []
/toolbox/ftp/search: /toolbox/ftp/search:
post: post:
consumes: consumes:

View File

@ -1,3 +1,5 @@
import { ReqPage } from '.';
export namespace Toolbox { export namespace Toolbox {
export interface DeviceBaseInfo { export interface DeviceBaseInfo {
dns: Array<string>; dns: Array<string>;
@ -77,6 +79,10 @@ export namespace Toolbox {
operate: string; operate: string;
} }
export interface FtpBaseInfo {
isActive: boolean;
isExist: boolean;
}
export interface FtpInfo { export interface FtpInfo {
id: number; id: number;
user: string; user: string;
@ -98,4 +104,16 @@ export namespace Toolbox {
path: string; path: string;
description: string; description: string;
} }
export interface FtpSearchLog extends ReqPage {
user: string;
operation: string;
}
export interface FtpLog {
ip: string;
user: string;
time: string;
operation: string;
status: string;
size: string;
}
} }

View File

@ -71,10 +71,18 @@ export const updateFail2banByFile = (param: UpdateByFile) => {
}; };
// ftp // ftp
export const getFtpBase = () => {
return http.get<Toolbox.FtpBaseInfo>(`/toolbox/ftp/base`);
};
export const searchFtpLog = (param: Toolbox.FtpSearchLog) => {
return http.post<ResPage<Toolbox.FtpLog>>(`/toolbox/ftp/log/search`, param);
};
export const searchFtp = (param: ReqPage) => { export const searchFtp = (param: ReqPage) => {
return http.post<ResPage<Toolbox.FtpInfo>>(`/toolbox/ftp/search`, param); return http.post<ResPage<Toolbox.FtpInfo>>(`/toolbox/ftp/search`, param);
}; };
export const operateFtp = (operate: string) => {
return http.post(`/toolbox/ftp/operate`, { operation: operate }, TimeoutEnum.T_5M);
};
export const syncFtp = () => { export const syncFtp = () => {
return http.post(`/toolbox/ftp/sync`); return http.post(`/toolbox/ftp/sync`);
}; };

View File

@ -1031,6 +1031,8 @@ const message = {
}, },
ftp: { ftp: {
ftp: 'FTP Account', ftp: 'FTP Account',
noFtp: 'FTP (pure-ftpd) service not detected, please refer to the official documentation for installation!',
operation: 'Perform [{0}] operation on FTP service, continue?',
enableHelper: enableHelper:
'Enabling the selected FTP account will restore its access permissions. Do you want to continue?', 'Enabling the selected FTP account will restore its access permissions. Do you want to continue?',
disableHelper: disableHelper:

View File

@ -978,6 +978,8 @@ const message = {
}, },
ftp: { ftp: {
ftp: 'FTP 帳戶', ftp: 'FTP 帳戶',
noFtp: '未檢測到 FTP (pure-ftpd) 服務請參考官方文檔進行安裝',
operation: ' FTP 服務進行 [{0}] 操作是否繼續',
enableHelper: '啟用選取的 FTP 帳號後 FTP 帳號將恢復訪問權限是否繼續操作', enableHelper: '啟用選取的 FTP 帳號後 FTP 帳號將恢復訪問權限是否繼續操作',
disableHelper: '停用選取的 FTP 帳號後 FTP 帳號將失去訪問權限是否繼續操作', disableHelper: '停用選取的 FTP 帳號後 FTP 帳號將失去訪問權限是否繼續操作',
syncHelper: '同步伺服器與資料庫中的 FTP 帳戶資料是否繼續操作', syncHelper: '同步伺服器與資料庫中的 FTP 帳戶資料是否繼續操作',

View File

@ -979,6 +979,8 @@ const message = {
}, },
ftp: { ftp: {
ftp: 'FTP 账户', ftp: 'FTP 账户',
noFtp: '未检测到 FTP (pure-ftpd) 服务请参考官方文档进行安装',
operation: ' FTP 服务进行 [{0}] 操作是否继续',
enableHelper: '启用选中的 FTP 账号后 FTP 账号恢复访问权限是否继续操作', enableHelper: '启用选中的 FTP 账号后 FTP 账号恢复访问权限是否继续操作',
disableHelper: '停用选中的 FTP 账号后 FTP 账号将失去访问权限是否继续操作', disableHelper: '停用选中的 FTP 账号后 FTP 账号将失去访问权限是否继续操作',
syncHelper: '同步服务器与数据库中的 FTP 账户数据是否继续操作', syncHelper: '同步服务器与数据库中的 FTP 账户数据是否继续操作',

View File

@ -173,6 +173,14 @@ export function computeSizeFromKB(size: number): string {
if (size < Math.pow(num, 3)) return (size / Math.pow(num, 2)).toFixed(2) + ' GB'; if (size < Math.pow(num, 3)) return (size / Math.pow(num, 2)).toFixed(2) + ' GB';
return (size / Math.pow(num, 3)).toFixed(2) + ' TB'; return (size / Math.pow(num, 3)).toFixed(2) + ' TB';
} }
export function computeSizeFromByte(size: number): string {
const num = 1024.0;
if (size < num) return size + ' B';
if (size < Math.pow(num, 2)) return (size / num).toFixed(2) + ' KB';
if (size < Math.pow(num, 3)) return (size / Math.pow(num, 2)).toFixed(2) + ' MB';
if (size < Math.pow(num, 4)) return (size / Math.pow(num, 2)).toFixed(2) + ' GB';
return (size / Math.pow(num, 5)).toFixed(2) + ' TB';
}
export function computeSizeFromKBs(size: number): string { export function computeSizeFromKBs(size: number): string {
const num = 1024.0; const num = 1024.0;

View File

@ -1,120 +1,168 @@
<template> <template>
<div> <div>
<LayoutContent v-loading="loading" title="FTP"> <div class="app-status" style="margin-top: 20px">
<template #toolbar> <el-card v-if="form.isExist">
<el-row> <div>
<el-col :xs="24" :sm="16" :md="16" :lg="16" :xl="16"> <el-tag effect="dark" type="success">FTP</el-tag>
<el-button type="primary" @click="onOpenDialog('add')"> <el-tag round class="status-content" v-if="form.isActive" type="success">
{{ $t('commons.button.add') }} FTP {{ $t('commons.status.running') }}
</el-tag>
<el-tag round class="status-content" v-if="!form.isActive" type="info">
{{ $t('commons.status.stopped') }}
</el-tag>
<span class="buttons">
<el-button v-if="form.isActive" type="primary" @click="onOperate('stop')" link>
{{ $t('commons.button.stop') }}
</el-button> </el-button>
<el-button @click="onSync()"> <el-button v-if="!form.isActive" type="primary" @click="onOperate('start')" link>
{{ $t('commons.button.sync') }} {{ $t('commons.button.start') }}
</el-button> </el-button>
<el-button plain :disabled="selects.length === 0" @click="onDelete(null)"> <el-divider direction="vertical" />
{{ $t('commons.button.delete') }} <el-button type="primary" @click="onOperate('restart')" link>
{{ $t('container.restart') }}
</el-button> </el-button>
</el-col> </span>
<el-col :xs="24" :sm="8" :md="8" :lg="8" :xl="8"> </div>
<TableSearch @search="search()" v-model:searchName="searchName" /> </el-card>
</el-col> </div>
</el-row> <div v-if="form.isExist">
</template> <LayoutContent v-loading="loading" title="FTP">
<template #main> <template #toolbar>
<ComplexTable <el-row>
:pagination-config="paginationConfig" <el-col :xs="24" :sm="16" :md="16" :lg="16" :xl="16">
v-model:selects="selects" <el-button type="primary" :disabled="!form.isActive" @click="onOpenDialog('add')">
@sort-change="search" {{ $t('commons.button.add') }} FTP
@search="search" </el-button>
:data="data" <el-button @click="onSync()" :disabled="!form.isActive">
> {{ $t('commons.button.sync') }}
<el-table-column type="selection" fix /> </el-button>
<el-table-column <el-button plain :disabled="selects.length === 0 || !form.isActive" @click="onDelete(null)">
:label="$t('commons.login.username')" {{ $t('commons.button.delete') }}
:min-width="60" </el-button>
prop="user" </el-col>
show-overflow-tooltip <el-col :xs="24" :sm="8" :md="8" :lg="8" :xl="8">
/> <TableSearch @search="search()" v-model:searchName="searchName" />
<el-table-column :label="$t('commons.login.password')" prop="password"> </el-col>
<template #default="{ row }"> </el-row>
<div v-if="row.password.length === 0">-</div> </template>
<div v-else class="flex items-center"> <template #main>
<div class="star-center" v-if="!row.showPassword"> <ComplexTable
<span>**********</span> :pagination-config="paginationConfig"
</div> v-model:selects="selects"
<div> @sort-change="search"
<span v-if="row.showPassword"> @search="search"
{{ row.password }} :data="data"
</span> >
<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> </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 <el-button
v-if="!row.showPassword" v-if="row.status === 'Enable'"
@click="onChangeStatus(row, 'disable')"
link link
@click="row.showPassword = true" icon="VideoPlay"
icon="View" type="success"
class="ml-1.5" >
></el-button> {{ $t('commons.status.enabled') }}
</el-button>
<el-button <el-button
v-if="row.showPassword" v-if="row.status === 'Disable'"
icon="VideoPause"
@click="onChangeStatus(row, 'enable')"
link link
@click="row.showPassword = false" type="danger"
icon="Hide" >
class="ml-1.5" {{ $t('commons.status.disabled') }}
></el-button> </el-button>
<div> </template>
<CopyButton :content="row.password" type="icon" /> </el-table-column>
</div> <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>
</div>
<div v-else>
<LayoutContent title="FTP" :divider="true">
<template #main>
<div class="app-warn">
<div>
<span>{{ $t('toolbox.ftp.noFtp') }}</span>
<span @click="toDoc">
<el-icon class="ml-2"><Position /></el-icon>
{{ $t('firewall.quickJump') }}
</span>
<div>
<img src="@/assets/images/no_app.svg" />
</div> </div>
</template> </div>
</el-table-column> </div>
<el-table-column :label="$t('commons.table.status')" :min-width="60" prop="status"> </template>
<template #default="{ row }"> </LayoutContent>
<el-tag v-if="row.status === 'deleted'" type="info">{{ $t('database.isDelete') }}</el-tag> </div>
<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()" /> <OpDialog ref="opRef" @search="search" @submit="onSubmitDelete()" />
<OperateDialog @search="search" ref="dialogRef" /> <OperateDialog @search="search" ref="dialogRef" />
<LogDialog ref="dialogLogRef" />
</div> </div>
</template> </template>
@ -123,8 +171,9 @@ import { onMounted, reactive, ref } from 'vue';
import i18n from '@/lang'; import i18n from '@/lang';
import { dateFormat } from '@/utils/util'; import { dateFormat } from '@/utils/util';
import { MsgSuccess } from '@/utils/message'; import { MsgSuccess } from '@/utils/message';
import { deleteFtp, searchFtp, updateFtp, syncFtp } 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 OperateDialog from '@/views/toolbox/ftp/operate/index.vue';
import LogDialog from '@/views/toolbox/ftp/log/index.vue';
import { Toolbox } from '@/api/interface/toolbox'; import { Toolbox } from '@/api/interface/toolbox';
const loading = ref(); const loading = ref();
@ -141,30 +190,72 @@ const paginationConfig = reactive({
}); });
const searchName = ref(); const searchName = ref();
const form = reactive({
isActive: false,
isExist: false,
});
const opRef = ref(); const opRef = ref();
const dialogRef = ref(); const dialogRef = ref();
const operateIDs = ref(); const operateIDs = ref();
const dialogLogRef = ref();
const search = async (column?: any) => { 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; loading.value = true;
await searchFtp(params) await getFtpBase()
.then((res) => { .then(async (res) => {
loading.value = false; form.isActive = res.data.isActive;
data.value = res.data.items || []; form.isExist = res.data.isExist;
paginationConfig.total = res.data.total; 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,
};
await searchFtp(params)
.then((res) => {
loading.value = false;
data.value = res.data.items || [];
paginationConfig.total = res.data.total;
})
.catch(() => {
loading.value = false;
});
}) })
.catch(() => { .catch(() => {
loading.value = false; loading.value = false;
}); });
}; };
const toDoc = () => {
window.open('https://1panel.cn/docs/user_manual/toolbox/ftp/', '_blank', 'noopener,noreferrer');
};
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', {
confirmButtonText: i18n.global.t('commons.button.confirm'),
cancelButtonText: i18n.global.t('commons.button.cancel'),
type: 'info',
})
.then(async () => {
loading.value = true;
await operateFtp(operation)
.then(() => {
loading.value = false;
MsgSuccess(i18n.global.t('commons.msg.operationSuccess'));
search();
})
.catch(() => {
loading.value = false;
});
})
.catch(() => {
search();
});
};
const onChangeStatus = async (row: Toolbox.FtpInfo, status: string) => { const onChangeStatus = async (row: Toolbox.FtpInfo, status: string) => {
ElMessageBox.confirm(i18n.global.t('toolbox.ftp.' + status + 'Helper'), i18n.global.t('cronjob.changeStatus'), { ElMessageBox.confirm(i18n.global.t('toolbox.ftp.' + status + 'Helper'), i18n.global.t('cronjob.changeStatus'), {
confirmButtonText: i18n.global.t('commons.button.confirm'), confirmButtonText: i18n.global.t('commons.button.confirm'),
@ -251,6 +342,15 @@ const buttons = [
onOpenDialog('edit', row); onOpenDialog('edit', row);
}, },
}, },
{
label: i18n.global.t('commons.button.log'),
disabled: (row: Toolbox.FtpInfo) => {
return row.status === 'deleted';
},
click: (row: Toolbox.FtpInfo) => {
dialogLogRef.value!.acceptParams({ user: row.user });
},
},
{ {
label: i18n.global.t('commons.button.delete'), label: i18n.global.t('commons.button.delete'),
disabled: (row: Toolbox.FtpInfo) => { disabled: (row: Toolbox.FtpInfo) => {

View File

@ -0,0 +1,119 @@
<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="FTP" :resource="paginationConfig.user" :back="handleClose" />
</template>
<el-select @change="search" class="p-w-200" clearable v-model="paginationConfig.operation">
<template #prefix>{{ $t('container.lines') }}</template>
<el-option value="PUT" :label="$t('file.upload')" />
<el-option value="GET" :label="$t('file.download')" />
</el-select>
<ComplexTable :pagination-config="paginationConfig" :data="data" @search="search">
<el-table-column label="ip" prop="ip" />
<el-table-column :label="$t('commons.login.username')" prop="user" />
<el-table-column :label="$t('commons.table.status')" show-overflow-tooltip prop="status">
<template #default="{ row }">
<el-tag v-if="row.status === '200'">{{ $t('commons.status.success') }}</el-tag>
<el-tag v-else type="danger">{{ $t('commons.status.failed') }}</el-tag>
</template>
</el-table-column>
<el-table-column :label="$t('commons.table.operate')" show-overflow-tooltip>
<template #default="{ row }">
{{ loadFileName(row.operation) }}
</template>
</el-table-column>
<el-table-column :label="$t('file.file')" show-overflow-tooltip>
<template #default="{ row }">
{{ loadOperation(row.operation) }}
</template>
</el-table-column>
<el-table-column :label="$t('file.size')" show-overflow-tooltip prop="size">
<template #default="{ row }">
{{ computeSizeFromByte(Number(row.size)) }}
</template>
</el-table-column>
<el-table-column :label="$t('commons.table.date')" prop="time" show-overflow-tooltip />
</ComplexTable>
<template #footer>
<span class="dialog-footer">
<el-button @click="drawerVisible = false">{{ $t('commons.button.cancel') }}</el-button>
</span>
</template>
</el-drawer>
</template>
<script lang="ts" setup>
import { reactive, ref } from 'vue';
import DrawerHeader from '@/components/drawer-header/index.vue';
import { searchFtpLog } from '@/api/modules/toolbox';
import { computeSizeFromByte } from '@/utils/util';
import i18n from '@/lang';
const paginationConfig = reactive({
cacheSizeKey: 'ftp-log-page-size',
currentPage: 1,
pageSize: 10,
total: 0,
user: '',
operation: '',
});
const data = ref();
interface DialogProps {
user: string;
}
const loading = ref();
const drawerVisible = ref(false);
const acceptParams = (params: DialogProps): void => {
paginationConfig.user = params.user;
search();
drawerVisible.value = true;
};
const handleClose = () => {
drawerVisible.value = false;
};
const search = async () => {
let params = {
user: paginationConfig.user,
operation: paginationConfig.operation,
page: paginationConfig.currentPage,
pageSize: paginationConfig.pageSize,
};
loading.value = true;
await searchFtpLog(params)
.then((res) => {
loading.value = false;
data.value = res.data.items || [];
paginationConfig.total = res.data.total;
})
.catch(() => {
loading.value = false;
});
};
const loadFileName = (operation: string) => {
if (operation.startsWith('"PUT')) {
return i18n.global.t('file.upload');
}
if (operation.startsWith('"GET')) {
return i18n.global.t('file.download');
}
};
const loadOperation = (operation: string) => {
return operation.replaceAll('"', '').replaceAll('PUT', '').replaceAll('GET', '');
};
defineExpose({
acceptParams,
});
</script>