refactor: 机器文件操作优化

This commit is contained in:
meilin.huang
2023-07-05 00:26:00 +08:00
parent 3266039aaf
commit f4ac6d8360
12 changed files with 164 additions and 50 deletions

View File

@@ -3,21 +3,21 @@
<el-dialog :title="title" v-model="dialogVisible" :show-close="true" :before-close="handleClose" width="800px"> <el-dialog :title="title" v-model="dialogVisible" :show-close="true" :before-close="handleClose" width="800px">
<div class="toolbar"> <div class="toolbar">
<div style="float: right"> <div style="float: right">
<el-button v-auth="'machine:file:add'" type="primary" @click="add" icon="plus" size="small" plain>添加 <el-button v-auth="'machine:file:add'" type="primary" @click="add" icon="plus" plain>添加
</el-button> </el-button>
</div> </div>
</div> </div>
<el-table :data="fileTable" stripe style="width: 100%" v-loading="loading"> <el-table :data="fileTable" stripe style="width: 100%" v-loading="loading">
<el-table-column prop="name" label="名称" width> <el-table-column prop="name" label="名称" min-width="70px">
<template #default="scope"> <template #default="scope">
<el-input v-model="scope.row.name" size="small" :disabled="scope.row.id != null" clearable> <el-input v-model="scope.row.name" :disabled="scope.row.id != null" clearable>
</el-input> </el-input>
</template> </template>
</el-table-column> </el-table-column>
<el-table-column prop="name" label="类型" min-width="50px"> <el-table-column prop="name" label="类型" min-width="50px">
<template #default="scope"> <template #default="scope">
<el-select :disabled="scope.row.id != null" size="small" v-model="scope.row.type" <el-select :disabled="scope.row.id != null" v-model="scope.row.type" style="width: 100px"
style="width: 100px" placeholder="请选择"> placeholder="请选择">
<el-option v-for="item in enums.FileTypeEnum as any" :key="item.value" :label="item.label" <el-option v-for="item in enums.FileTypeEnum as any" :key="item.value" :label="item.label"
:value="item.value"></el-option> :value="item.value"></el-option>
</el-select> </el-select>
@@ -25,18 +25,18 @@
</el-table-column> </el-table-column>
<el-table-column prop="path" label="路径" width> <el-table-column prop="path" label="路径" width>
<template #default="scope"> <template #default="scope">
<el-input v-model="scope.row.path" :disabled="scope.row.id != null" size="small" clearable> <el-input v-model="scope.row.path" :disabled="scope.row.id != null" clearable>
</el-input> </el-input>
</template> </template>
</el-table-column> </el-table-column>
<el-table-column label="操作" width> <el-table-column label="操作" width>
<template #default="scope"> <template #default="scope">
<el-button v-if="scope.row.id == null" @click="addFiles(scope.row)" type="success" <el-button v-if="scope.row.id == null" @click="addFiles(scope.row)" type="success"
icon="success-filled" size="small" plain>确定</el-button> icon="success-filled" plain>确定</el-button>
<el-button v-if="scope.row.id != null" @click="getConf(scope.row)" type="primary" icon="tickets" <el-button v-if="scope.row.id != null" @click="getConf(scope.row)" type="primary" icon="tickets"
size="small" plain>查看</el-button> plain>查看</el-button>
<el-button v-auth="'machine:file:del'" type="danger" @click="deleteRow(scope.$index, scope.row)" <el-button v-auth="'machine:file:del'" type="danger" @click="deleteRow(scope.$index, scope.row)"
icon="delete" size="small" plain>删除</el-button> icon="delete" plain>删除</el-button>
</template> </template>
</el-table-column> </el-table-column>
</el-table> </el-table>
@@ -50,24 +50,24 @@
<el-dialog :title="tree.title" v-model="tree.visible" :close-on-click-modal="false" width="70%"> <el-dialog :title="tree.title" v-model="tree.visible" :close-on-click-modal="false" width="70%">
<el-progress v-if="uploadProgressShow" style="width: 90%; margin-left: 20px" :text-inside="true" <el-progress v-if="uploadProgressShow" style="width: 90%; margin-left: 20px" :text-inside="true"
:stroke-width="20" :percentage="progressNum" /> :stroke-width="20" :percentage="progressNum" />
<div style="height: 45vh; overflow: auto"> <div style="height: 55vh; overflow: auto">
<el-tree v-if="tree.visible" ref="fileTree" :highlight-current="true" :load="loadNode" :props="treeProps" <el-tree v-if="tree.visible" ref="fileTree" :highlight-current="true" :load="loadNode" :props="treeProps"
lazy node-key="id" :expand-on-click-node="true"> lazy node-key="id" :expand-on-click-node="false">
<template #default="{ node, data }"> <template #default="{ node, data }">
<span class="custom-tree-node"> <span class="custom-tree-node">
<el-dropdown size="small" @visible-change="getFilePath(data, $event)" trigger="contextmenu"> <el-dropdown size="small" @visible-change="getFilePath(data, $event)" trigger="contextmenu">
<span class="el-dropdown-link"> <span class="el-dropdown-link">
<span v-if="data.type == 'd' && !node.expanded"> <span v-if="data.type == 'd' && !node.expanded">
<SvgIcon name="folder" /> <SvgIcon :size="15" name="folder" />
</span> </span>
<span v-if="data.type == 'd' && node.expanded"> <span v-if="data.type == 'd' && node.expanded">
<SvgIcon name="folder-opened" /> <SvgIcon :size="15" name="folder-opened" />
</span> </span>
<span v-if="data.type == '-'"> <span v-if="data.type == '-'">
<SvgIcon name="document" /> <SvgIcon :size="15" name="document" />
</span> </span>
<span> <span class="ml5" style="font-weight: bold;">
{{ node.label }} {{ node.label }}
</span> </span>
</span> </span>
@@ -114,10 +114,28 @@
</template> </template>
</el-dropdown> </el-dropdown>
<span style="display: inline-block" class="ml15"> <span style="display: inline-block" class="ml15">
<span style="color: #67c23a" v-if="data.type == '-'">[{{ formatFileSize(data.size) <span style="color: #67c23a;font-weight: bold;" v-if="data.type == '-'">
}}]</span> [{{ formatFileSize(data.size) }}]
<span v-if="data.mode" style="color: #67c23a">&nbsp;[{{ data.mode }} {{ data.modTime </span>
}}]</span> <span style="color: #67c23a;font-weight: bold;" v-if="data.type == 'd' && data.dirSize">
[{{ data.dirSize }}]
</span>
<span style="color: #67c23a;font-weight: bold;" v-if="data.type == 'd' && !data.dirSize">
[<el-button @click="getDirSize(data)" type="primary" link
:loading="data.loadingDirSize">size</el-button>]
</span>
<el-popover placement="top-start" :title="`${data.path}-文件详情`" :width="520" trigger="click"
@show="showFileStat(data)">
<template #reference>
<span style="color: #67c23a;font-weight: bold;">
[<el-button @click="showFileStat(data)" type="primary" link
:loading="data.loadingStat">stat</el-button>]
</span>
</template>
<el-input :input-style="{ color: 'black' }" disabled autosize v-model="data.stat"
type="textarea" />
</el-popover>
</span> </span>
</span> </span>
</template> </template>
@@ -444,6 +462,37 @@ const loadNode = async (node: any, resolve: any) => {
return resolve(res); return resolve(res);
}; };
const getDirSize = async (data: any) => {
try {
data.loadingDirSize = true;
const res = await machineApi.dirSize.request({
machineId: props.machineId,
fileId: state.tree.folder.id,
path: data.path
})
data.dirSize = res;
} finally {
data.loadingDirSize = false;
}
}
const showFileStat = async (data: any) => {
try {
if (data.stat) {
return;
}
data.loadingStat = true;
const res = await machineApi.fileStat.request({
machineId: props.machineId,
fileId: state.tree.folder.id,
path: data.path
})
data.stat = res;
} finally {
data.loadingStat = false;
}
}
const showCreateFileDialog = (node: any) => { const showCreateFileDialog = (node: any) => {
isTrue(node.expanded, '请先点击展开该节点后再创建'); isTrue(node.expanded, '请先点击展开该节点后再创建');
state.createFileDialog.node = node; state.createFileDialog.node = node;
@@ -595,4 +644,4 @@ const formatFileSize = (size: any) => {
return '-'; return '-';
}; };
</script> </script>
<style lang="scss"></style> <style lang="scss"></style>

View File

@@ -2,7 +2,7 @@
<div class="mock-data-dialog"> <div class="mock-data-dialog">
<el-dialog :title="title" v-model="dialogVisible" :close-on-click-modal="false" :before-close="cancel" <el-dialog :title="title" v-model="dialogVisible" :close-on-click-modal="false" :before-close="cancel"
:show-close="true" :destroy-on-close="true" width="900px"> :show-close="true" :destroy-on-close="true" width="900px">
<el-form :model="form" ref="scriptForm" label-width="auto" size="small"> <el-form :model="form" ref="scriptForm" label-width="auto" >
<el-form-item prop="method" label="名称"> <el-form-item prop="method" label="名称">
<el-input v-model="form.name" placeholder="请输入名称"></el-input> <el-input v-model="form.name" placeholder="请输入名称"></el-input>
</el-form-item> </el-form-item>
@@ -19,7 +19,7 @@
</el-form-item> </el-form-item>
<el-row style="margin-left: 30px; margin-bottom: 5px"> <el-row style="margin-left: 30px; margin-bottom: 5px">
<el-button @click="onAddParam" size="small" type="success">新增占位符参数</el-button> <el-button @click="onAddParam" type="success">新增占位符参数</el-button>
</el-row> </el-row>
<el-form-item :key="param" v-for="(param, index) in params" prop="params" :label="`参数${index + 1}`"> <el-form-item :key="param" v-for="(param, index) in params" prop="params" :label="`参数${index + 1}`">
<el-row> <el-row>
@@ -48,7 +48,7 @@
<el-divider direction="vertical" border-style="dashed" /> <el-divider direction="vertical" border-style="dashed" />
</span> </span>
<el-col :span="2"> <el-col :span="2">
<el-button @click="onDeleteParam(index)" size="small" type="danger">删除</el-button> <el-button @click="onDeleteParam(index)" type="danger">删除</el-button>
</el-col> </el-col>
</el-row> </el-row>
</el-form-item> </el-form-item>

View File

@@ -4,23 +4,22 @@
:before-close="handleClose" width="60%"> :before-close="handleClose" width="60%">
<div class="toolbar"> <div class="toolbar">
<div style="float: left"> <div style="float: left">
<el-select v-model="type" @change="getScripts" size="small" placeholder="请选择"> <el-select v-model="type" @change="getScripts" placeholder="请选择">
<el-option :key="0" label="私有" :value="0"> </el-option> <el-option :key="0" label="私有" :value="0"> </el-option>
<el-option :key="1" label="公共" :value="1"> </el-option> <el-option :key="1" label="公共" :value="1"> </el-option>
</el-select> </el-select>
</div> </div>
<div style="float: right"> <div style="float: right">
<el-button @click="editScript(currentData)" :disabled="currentId == null" type="primary" icon="tickets" <el-button @click="editScript(currentData)" :disabled="currentId == null" type="primary" icon="tickets"
size="small" plain>查看</el-button> plain>查看</el-button>
<el-button v-auth="'machine:script:save'" type="primary" @click="editScript(null)" icon="plus" <el-button v-auth="'machine:script:save'" type="primary" @click="editScript(null)" icon="plus"
size="small" plain>添加</el-button> plain>添加</el-button>
<el-button v-auth="'machine:script:del'" :disabled="currentId == null" type="danger" <el-button v-auth="'machine:script:del'" :disabled="currentId == null" type="danger"
@click="deleteRow(currentData)" icon="delete" size="small" plain>删除</el-button> @click="deleteRow(currentData)" icon="delete" plain>删除</el-button>
</div> </div>
</div> </div>
<el-table :data="scriptTable" @current-change="choose" stripe border size="small" v-loading="loading" <el-table :data="scriptTable" @current-change="choose" stripe border v-loading="loading" style="width: 100%">
style="width: 100%">
<el-table-column label="选择" width="55px"> <el-table-column label="选择" width="55px">
<template #default="scope"> <template #default="scope">
<el-radio v-model="currentId" :label="scope.row.id"> <el-radio v-model="currentId" :label="scope.row.id">
@@ -37,11 +36,11 @@
</el-table-column> </el-table-column>
<el-table-column label="操作"> <el-table-column label="操作">
<template #default="scope"> <template #default="scope">
<el-button v-if="scope.row.id == null" type="success" icon="el-icon-success" size="small" plain> <el-button v-if="scope.row.id == null" type="success" icon="el-icon-success" plain>
确定</el-button> 确定</el-button>
<el-button v-auth="'machine:script:run'" v-if="scope.row.id != null" @click="runScript(scope.row)" <el-button v-auth="'machine:script:run'" v-if="scope.row.id != null" @click="runScript(scope.row)"
type="primary" icon="video-play" size="small" plain>执行 type="primary" icon="video-play" plain>执行
</el-button> </el-button>
</template> </template>
</el-table-column> </el-table-column>
@@ -54,7 +53,7 @@
</el-dialog> </el-dialog>
<el-dialog title="脚本参数" v-model="scriptParamsDialog.visible" width="400px"> <el-dialog title="脚本参数" v-model="scriptParamsDialog.visible" width="400px">
<el-form ref="paramsForm" :model="scriptParamsDialog.params" label-width="auto" size="small"> <el-form ref="paramsForm" :model="scriptParamsDialog.params" label-width="auto">
<el-form-item v-for="item in scriptParamsDialog.paramsFormItem as any" :key="item.name" :prop="item.model" <el-form-item v-for="item in scriptParamsDialog.paramsFormItem as any" :key="item.name" :prop="item.model"
:label="item.name" required> :label="item.name" required>
<el-input v-if="!item.options" v-model="scriptParamsDialog.params[item.model]" <el-input v-if="!item.options" v-model="scriptParamsDialog.params[item.model]"
@@ -68,7 +67,7 @@
</el-form> </el-form>
<template #footer> <template #footer>
<span class="dialog-footer"> <span class="dialog-footer">
<el-button type="primary" @click="hasParamsRun(currentData)" size="small"> </el-button> <el-button type="primary" @click="hasParamsRun(currentData)"> </el-button>
</span> </span>
</template> </template>
</el-dialog> </el-dialog>

View File

@@ -24,6 +24,8 @@ export const machineApi = {
// 获取配置文件列表 // 获取配置文件列表
files: Api.newGet("/machines/{id}/files"), files: Api.newGet("/machines/{id}/files"),
lsFile: Api.newGet("/machines/{machineId}/files/{fileId}/read-dir"), lsFile: Api.newGet("/machines/{machineId}/files/{fileId}/read-dir"),
dirSize: Api.newGet("/machines/{machineId}/files/{fileId}/dir-size"),
fileStat: Api.newGet("/machines/{machineId}/files/{fileId}/file-stat"),
rmFile: Api.newDelete("/machines/{machineId}/files/{fileId}/remove"), rmFile: Api.newDelete("/machines/{machineId}/files/{fileId}/remove"),
uploadFile: Api.newPost("/machines/{machineId}/files/{fileId}/upload?token={token}"), uploadFile: Api.newPost("/machines/{machineId}/files/{fileId}/upload?token={token}"),
fileContent: Api.newGet("/machines/{machineId}/files/{fileId}/read"), fileContent: Api.newGet("/machines/{machineId}/files/{fileId}/read"),

View File

@@ -76,7 +76,6 @@ func (m *MachineFile) CreateFile(rc *req.Ctx) {
m.MachineFileApp.CreateFile(fid, form.Path) m.MachineFileApp.CreateFile(fid, form.Path)
rc.ReqParam = fmt.Sprintf("%s -> 创建文件: %s", mi.GetLogDesc(), path) rc.ReqParam = fmt.Sprintf("%s -> 创建文件: %s", mi.GetLogDesc(), path)
} }
} }
func (m *MachineFile) ReadFileContent(rc *req.Ctx) { func (m *MachineFile) ReadFileContent(rc *req.Ctx) {
@@ -121,12 +120,10 @@ func (m *MachineFile) GetDirEntry(rc *req.Ctx) {
fisVO := make([]vo.MachineFileInfo, 0) fisVO := make([]vo.MachineFileInfo, 0)
for _, fi := range fis { for _, fi := range fis {
fisVO = append(fisVO, vo.MachineFileInfo{ fisVO = append(fisVO, vo.MachineFileInfo{
Name: fi.Name(), Name: fi.Name(),
Size: fi.Size(), Size: fi.Size(),
Path: readPath + fi.Name(), Path: readPath + fi.Name(),
Type: getFileType(fi.Mode()), Type: getFileType(fi.Mode()),
Mode: fi.Mode().String(),
ModTime: fi.ModTime().Format("2006-01-02 15:04:05"),
}) })
} }
sort.Sort(vo.MachineFileInfos(fisVO)) sort.Sort(vo.MachineFileInfos(fisVO))
@@ -134,6 +131,22 @@ func (m *MachineFile) GetDirEntry(rc *req.Ctx) {
rc.ReqParam = fmt.Sprintf("path: %s", readPath) rc.ReqParam = fmt.Sprintf("path: %s", readPath)
} }
func (m *MachineFile) GetDirSize(rc *req.Ctx) {
g := rc.GinCtx
fid := GetMachineFileId(g)
readPath := g.Query("path")
rc.ResData = m.MachineFileApp.GetDirSize(fid, readPath)
}
func (m *MachineFile) GetFileStat(rc *req.Ctx) {
g := rc.GinCtx
fid := GetMachineFileId(g)
readPath := g.Query("path")
rc.ResData = m.MachineFileApp.FileStat(fid, readPath)
}
func (m *MachineFile) WriteFileContent(rc *req.Ctx) { func (m *MachineFile) WriteFileContent(rc *req.Ctx) {
g := rc.GinCtx g := rc.GinCtx
fid := GetMachineFileId(g) fid := GetMachineFileId(g)

View File

@@ -70,7 +70,9 @@ func (m *MachineScript) RunMachineScript(rc *req.Ctx) {
res, err := cli.Run(script) res, err := cli.Run(script)
// 记录请求参数 // 记录请求参数
rc.ReqParam = fmt.Sprintf("[machine: %s, scriptId: %d, name: %s]", cli.GetMachine().GetLogDesc(), scriptId, ms.Name) rc.ReqParam = fmt.Sprintf("[machine: %s, scriptId: %d, name: %s]", cli.GetMachine().GetLogDesc(), scriptId, ms.Name)
biz.ErrIsNilAppendErr(err, "执行命令失败:%s") if res == "" {
biz.ErrIsNilAppendErr(err, "执行命令失败:%s")
}
rc.ResData = res rc.ResData = res
} }

View File

@@ -52,12 +52,10 @@ type MachineFileVO struct {
} }
type MachineFileInfo struct { type MachineFileInfo struct {
Name string `json:"name"` Name string `json:"name"`
Path string `json:"path"` Path string `json:"path"`
Size int64 `json:"size"` Size int64 `json:"size"`
Type string `json:"type"` Type string `json:"type"`
Mode string `json:"mode"`
ModTime string `json:"modTime"`
} }
type MachineFileInfos []MachineFileInfo type MachineFileInfos []MachineFileInfo

View File

@@ -44,6 +44,12 @@ type MachineFile interface {
// 读取目录 // 读取目录
ReadDir(fid uint64, path string) []fs.FileInfo ReadDir(fid uint64, path string) []fs.FileInfo
// 获取指定目录内容大小
GetDirSize(fid uint64, path string) string
// 获取文件stat
FileStat(fid uint64, path string) string
// 读取文件内容 // 读取文件内容
ReadFile(fileId uint64, path string) *sftp.File ReadFile(fileId uint64, path string) *sftp.File
@@ -110,6 +116,35 @@ func (m *machineFileAppImpl) ReadDir(fid uint64, path string) []fs.FileInfo {
return fis return fis
} }
func (m *machineFileAppImpl) GetDirSize(fid uint64, path string) string {
_, machineId := m.checkAndReturnPathMid(fid, path)
res, err := GetMachineApp().GetCli(machineId).Run(fmt.Sprintf("du -sh %s", path))
if err != nil {
// 若存在目录为空,则可能会返回如下内容。最后一行即为真正目录内容所占磁盘空间大小
//du: cannot access /proc/19087/fd/3: No such file or directory\n
//du: cannot access /proc/19087/fdinfo/3: No such file or directory\n
//18G /\n
if res == "" {
panic(biz.NewBizErr(fmt.Sprintf("获取目录大小失败: %s", err.Error())))
}
strs := strings.Split(res, "\n")
res = strs[len(strs)-2]
if !strings.Contains(res, "\t") {
panic(biz.NewBizErr(res))
}
}
// 返回 32K\t/tmp\n
return strings.Split(res, "\t")[0]
}
func (m *machineFileAppImpl) FileStat(fid uint64, path string) string {
_, machineId := m.checkAndReturnPathMid(fid, path)
res, err := GetMachineApp().GetCli(machineId).Run(fmt.Sprintf("stat -L %s", path))
biz.ErrIsNil(err, res)
return res
}
func (m *machineFileAppImpl) MkDir(fid uint64, path string) { func (m *machineFileAppImpl) MkDir(fid uint64, path string) {
path, machineId := m.checkAndReturnPathMid(fid, path) path, machineId := m.checkAndReturnPathMid(fid, path)
if !strings.HasSuffix(path, "/") { if !strings.HasSuffix(path, "/") {

View File

@@ -119,6 +119,7 @@ func (c *Cli) GetSession() (*ssh.Session, error) {
// 执行shell // 执行shell
// @param shell shell脚本命令 // @param shell shell脚本命令
// @return 返回执行成功或错误的消息
func (c *Cli) Run(shell string) (string, error) { func (c *Cli) Run(shell string) (string, error) {
session, err := c.GetSession() session, err := c.GetSession()
if err != nil { if err != nil {
@@ -128,7 +129,7 @@ func (c *Cli) Run(shell string) (string, error) {
defer session.Close() defer session.Close()
buf, err := session.CombinedOutput(shell) buf, err := session.CombinedOutput(shell)
if err != nil { if err != nil {
return "", err return string(buf), err
} }
return string(buf), nil return string(buf), nil
} }

View File

@@ -52,6 +52,14 @@ func InitMachineFileRouter(router *gin.RouterGroup) {
Handle(mf.GetDirEntry) Handle(mf.GetDirEntry)
}) })
machineFile.GET(":machineId/files/:fileId/dir-size", func(c *gin.Context) {
req.NewCtxWithGin(c).Handle(mf.GetDirSize)
})
machineFile.GET(":machineId/files/:fileId/file-stat", func(c *gin.Context) {
req.NewCtxWithGin(c).Handle(mf.GetFileStat)
})
writeFile := req.NewLogInfo("机器-修改文件内容").WithSave(true) writeFile := req.NewLogInfo("机器-修改文件内容").WithSave(true)
wfP := req.NewPermission("machine:file:write") wfP := req.NewPermission("machine:file:write")
machineFile.POST(":machineId/files/:fileId/write", func(c *gin.Context) { machineFile.POST(":machineId/files/:fileId/write", func(c *gin.Context) {

View File

@@ -52,7 +52,7 @@ func PageQuery[T any](q *QueryCond, pageParam *model.PageParam, toModels T) *mod
gdb := q.GenGdb() gdb := q.GenGdb()
var count int64 var count int64
err := gdb.Count(&count).Error err := gdb.Count(&count).Error
biz.ErrIsNilAppendErr(err, " 查询错误%s") biz.ErrIsNilAppendErr(err, "查询错误: %s")
if count == 0 { if count == 0 {
return model.EmptyPageResult[T]() return model.EmptyPageResult[T]()
} }
@@ -60,7 +60,7 @@ func PageQuery[T any](q *QueryCond, pageParam *model.PageParam, toModels T) *mod
page := pageParam.PageNum page := pageParam.PageNum
pageSize := pageParam.PageSize pageSize := pageParam.PageSize
err = gdb.Limit(pageSize).Offset((page - 1) * pageSize).Find(toModels).Error err = gdb.Limit(pageSize).Offset((page - 1) * pageSize).Find(toModels).Error
biz.ErrIsNil(err, "查询失败") biz.ErrIsNilAppendErr(err, "查询失败: %s")
return &model.PageResult[T]{Total: count, List: toModels} return &model.PageResult[T]{Total: count, List: toModels}
} }

7
server/pkg/utils/time.go Normal file
View File

@@ -0,0 +1,7 @@
package utils
import "time"
func DefaultTimeFormat(time time.Time) string {
return time.Format("2006-01-02 15:04:05")
}