Browse Source

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

pull/5076/head
ssongliu 6 months ago committed by GitHub
parent
commit
65f92bf0c3
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 64
      backend/app/api/v1/ftp.go
  2. 15
      backend/app/dto/ftp.go
  3. 43
      backend/app/service/ftp.go
  4. 3
      backend/router/ro_toolbox.go
  5. 2
      backend/utils/toolbox/fail2ban.go
  6. 118
      backend/utils/toolbox/pure-ftpd.go
  7. 128
      cmd/server/docs/docs.go
  8. 128
      cmd/server/docs/swagger.json
  9. 81
      cmd/server/docs/swagger.yaml
  10. 18
      frontend/src/api/interface/toolbox.ts
  11. 10
      frontend/src/api/modules/toolbox.ts
  12. 2
      frontend/src/lang/modules/en.ts
  13. 2
      frontend/src/lang/modules/tw.ts
  14. 2
      frontend/src/lang/modules/zh.ts
  15. 8
      frontend/src/utils/util.ts
  16. 332
      frontend/src/views/toolbox/ftp/index.vue
  17. 119
      frontend/src/views/toolbox/ftp/log/index.vue

64
backend/app/api/v1/ftp.go

@ -9,6 +9,70 @@ import (
"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
// @Summary Page FTP user
// @Description 获取 FTP 账户列表分页

15
backend/app/dto/ftp.go

@ -1,6 +1,8 @@
package dto
import "time"
import (
"time"
)
type FtpInfo struct {
ID uint `json:"id"`
@ -13,6 +15,17 @@ type FtpInfo struct {
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 {
User string `json:"user" validate:"required"`
Password string `json:"password" validate:"required"`

43
backend/app/service/ftp.go

@ -13,17 +13,60 @@ import (
type FtpService struct{}
type IFtpService interface {
LoadBaseInfo() (dto.FtpBaseInfo, error)
SearchWithPage(search dto.SearchWithPage) (int64, interface{}, error)
Operate(operation string) error
Create(req dto.FtpCreate) error
Delete(req dto.BatchDeleteReq) error
Update(req dto.FtpUpdate) error
Sync() error
LoadLog(req dto.FtpLogSearch) (int64, interface{}, error)
}
func NewIFtpService() IFtpService {
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) {
total, lists, err := ftpRepo.Page(req.Page, req.PageSize, ftpRepo.WithByUser(req.Info), commonRepo.WithOrderBy("created_at desc"))
if err != nil {

3
backend/router/ro_toolbox.go

@ -37,6 +37,9 @@ func (s *ToolboxRouter) InitRouter(Router *gin.RouterGroup) {
toolboxRouter.POST("/fail2ban/update", baseApi.UpdateFail2BanConf)
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", baseApi.CreateFtp)
toolboxRouter.POST("/ftp/update", baseApi.UpdateFtp)

2
backend/utils/toolbox/fail2ban.go

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

118
backend/utils/toolbox/pure-ftpd.go

@ -2,8 +2,13 @@ package toolbox
import (
"errors"
"fmt"
"os"
"os/user"
"path"
"path/filepath"
"strings"
"time"
"github.com/1Panel-dev/1Panel/backend/constant"
"github.com/1Panel-dev/1Panel/backend/utils/cmd"
@ -15,12 +20,14 @@ type Ftp struct {
}
type FtpClient interface {
Status() (bool, error)
Status() (bool, bool)
Operate(operate string) error
LoadList() ([]FtpList, error)
UserAdd(username, path, passwd string) error
UserDel(username string) error
SetPasswd(username, passwd string) error
Reload() error
LoadLogs() ([]FtpLog, error)
}
func NewFtpClient() (*Ftp, error) {
@ -54,8 +61,24 @@ func NewFtpClient() (*Ftp, error) {
return &Ftp{DefaultUser: "1panel"}, nil
}
func (f *Ftp) Status() (bool, error) {
return systemctl.IsActive("pure-ftpd.service")
func (f *Ftp) Status() (bool, bool) {
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 {
@ -141,3 +164,92 @@ func (f *Ftp) Reload() error {
}
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"`
}

128
cmd/server/docs/docs.go

@ -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": {
"post": {
"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": {
"post": {
"security": [
@ -16182,6 +16278,17 @@ const docTemplate = `{
}
}
},
"dto.FtpBaseInfo": {
"type": "object",
"properties": {
"isActive": {
"type": "boolean"
},
"isExist": {
"type": "boolean"
}
}
},
"dto.FtpCreate": {
"type": "object",
"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": {
"type": "object",
"required": [

128
cmd/server/docs/swagger.json

@ -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": {
"post": {
"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": {
"post": {
"security": [
@ -16175,6 +16271,17 @@
}
}
},
"dto.FtpBaseInfo": {
"type": "object",
"properties": {
"isActive": {
"type": "boolean"
},
"isExist": {
"type": "boolean"
}
}
},
"dto.FtpCreate": {
"type": "object",
"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": {
"type": "object",
"required": [

81
cmd/server/docs/swagger.yaml

@ -1202,6 +1202,13 @@ definitions:
- type
- vars
type: object
dto.FtpBaseInfo:
properties:
isActive:
type: boolean
isExist:
type: boolean
type: object
dto.FtpCreate:
properties:
description:
@ -1217,6 +1224,20 @@ definitions:
- path
- user
type: object
dto.FtpLogSearch:
properties:
operation:
type: string
page:
type: integer
pageSize:
type: integer
user:
type: string
required:
- page
- pageSize
type: object
dto.FtpUpdate:
properties:
description:
@ -12456,6 +12477,19 @@ paths:
formatEN: create FTP [user][path]
formatZH: 创建 FTP 账户 [user][path]
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:
post:
consumes:
@ -12489,6 +12523,53 @@ paths:
formatEN: delete FTP users [users]
formatZH: 删除 FTP 账户 [users]
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:
post:
consumes:

18
frontend/src/api/interface/toolbox.ts

@ -1,3 +1,5 @@
import { ReqPage } from '.';
export namespace Toolbox {
export interface DeviceBaseInfo {
dns: Array<string>;
@ -77,6 +79,10 @@ export namespace Toolbox {
operate: string;
}
export interface FtpBaseInfo {
isActive: boolean;
isExist: boolean;
}
export interface FtpInfo {
id: number;
user: string;
@ -98,4 +104,16 @@ export namespace Toolbox {
path: 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;
}
}

10
frontend/src/api/modules/toolbox.ts

@ -71,10 +71,18 @@ export const updateFail2banByFile = (param: UpdateByFile) => {
};
// 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) => {
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 = () => {
return http.post(`/toolbox/ftp/sync`);
};

2
frontend/src/lang/modules/en.ts

@ -1031,6 +1031,8 @@ const message = {
},
ftp: {
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:
'Enabling the selected FTP account will restore its access permissions. Do you want to continue?',
disableHelper:

2
frontend/src/lang/modules/tw.ts

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

2
frontend/src/lang/modules/zh.ts

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

8
frontend/src/utils/util.ts

@ -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';
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 {
const num = 1024.0;

332
frontend/src/views/toolbox/ftp/index.vue

@ -1,120 +1,168 @@
<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
<div class="app-status" style="margin-top: 20px">
<el-card v-if="form.isExist">
<div>
<el-tag effect="dark" type="success">FTP</el-tag>
<el-tag round class="status-content" v-if="form.isActive" type="success">
{{ $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 @click="onSync()">
{{ $t('commons.button.sync') }}
<el-button v-if="!form.isActive" type="primary" @click="onOperate('start')" link>
{{ $t('commons.button.start') }}
</el-button>
<el-button plain :disabled="selects.length === 0" @click="onDelete(null)">
{{ $t('commons.button.delete') }}
<el-divider direction="vertical" />
<el-button type="primary" @click="onOperate('restart')" link>
{{ $t('container.restart') }}
</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>
</span>
</div>
</el-card>
</div>
<div v-if="form.isExist">
<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" :disabled="!form.isActive" @click="onOpenDialog('add')">
{{ $t('commons.button.add') }} FTP
</el-button>
<el-button @click="onSync()" :disabled="!form.isActive">
{{ $t('commons.button.sync') }}
</el-button>
<el-button plain :disabled="selects.length === 0 || !form.isActive" @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.showPassword"
v-if="row.status === 'Enable'"
@click="onChangeStatus(row, 'disable')"
link
@click="row.showPassword = true"
icon="View"
class="ml-1.5"
></el-button>
icon="VideoPlay"
type="success"
>
{{ $t('commons.status.enabled') }}
</el-button>
<el-button
v-if="row.showPassword"
v-if="row.status === 'Disable'"
icon="VideoPause"
@click="onChangeStatus(row, 'enable')"
link
@click="row.showPassword = false"
icon="Hide"
class="ml-1.5"
></el-button>
<div>
<CopyButton :content="row.password" type="icon" />
</div>
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>
</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>
</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>
</div>
</div>
</template>
</LayoutContent>
</div>
<OpDialog ref="opRef" @search="search" @submit="onSubmitDelete()" />
<OperateDialog @search="search" ref="dialogRef" />
<LogDialog ref="dialogLogRef" />
</div>
</template>
@ -123,8 +171,9 @@ 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 { 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';
const loading = ref();
@ -141,30 +190,72 @@ const paginationConfig = reactive({
});
const searchName = ref();
const form = reactive({
isActive: false,
isExist: false,
});
const opRef = ref();
const dialogRef = ref();
const operateIDs = ref();
const dialogLogRef = 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;
await getFtpBase()
.then(async (res) => {
form.isActive = res.data.isActive;
form.isExist = res.data.isExist;
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(() => {
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) => {
ElMessageBox.confirm(i18n.global.t('toolbox.ftp.' + status + 'Helper'), i18n.global.t('cronjob.changeStatus'), {
confirmButtonText: i18n.global.t('commons.button.confirm'),
@ -251,6 +342,15 @@ const buttons = [
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'),
disabled: (row: Toolbox.FtpInfo) => {

119
frontend/src/views/toolbox/ftp/log/index.vue

@ -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>
Loading…
Cancel
Save