mirror of
https://gitee.com/dromara/mayfly-go
synced 2025-11-03 07:50:25 +08:00
feat: 新增linux文件夹创建&删除&其他优化
This commit is contained in:
@@ -44,7 +44,9 @@ export default defineComponent({
|
||||
state.isShowSearch = true;
|
||||
initTageView();
|
||||
nextTick(() => {
|
||||
layoutMenuAutocompleteRef.value.focus();
|
||||
setTimeout(() => {
|
||||
layoutMenuAutocompleteRef.value.focus();
|
||||
});
|
||||
});
|
||||
};
|
||||
// 搜索弹窗关闭
|
||||
|
||||
@@ -341,7 +341,7 @@ export default defineComponent({
|
||||
|
||||
onMounted(async () => {
|
||||
search();
|
||||
state.projects = (await projectApi.projects.request({ pageNum: 1, pageSize: 100 })).list;
|
||||
state.projects = await projectApi.accountProjects.request(null);
|
||||
});
|
||||
|
||||
const choose = (item: any) => {
|
||||
|
||||
@@ -135,7 +135,9 @@ export default defineComponent({
|
||||
state.db = props.db;
|
||||
state.dialogVisible = true;
|
||||
nextTick(() => {
|
||||
remarkInputRef.value?.focus();
|
||||
setTimeout(() => {
|
||||
remarkInputRef.value?.focus();
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
@@ -96,6 +96,12 @@
|
||||
<el-link type="info" icon="view" :underline="false">查看</el-link>
|
||||
</el-dropdown-item>
|
||||
|
||||
<span v-auth="'machine:file:write'">
|
||||
<el-dropdown-item @click="showCreateFileDialog(node, data)" v-if="data.type == 'd'">
|
||||
<el-link type="primary" icon="document" :underline="false" style="margin-left: 2px">新建</el-link>
|
||||
</el-dropdown-item>
|
||||
</span>
|
||||
|
||||
<span v-auth="'machine:file:upload'">
|
||||
<el-dropdown-item v-if="data.type == 'd'">
|
||||
<el-upload
|
||||
@@ -133,6 +139,35 @@
|
||||
</div>
|
||||
</el-dialog>
|
||||
|
||||
<el-dialog
|
||||
:destroy-on-close="true"
|
||||
title="新建文件"
|
||||
v-model="createFileDialog.visible"
|
||||
:before-close="closeCreateFileDialog"
|
||||
:close-on-click-modal="false"
|
||||
top="5vh"
|
||||
width="400px"
|
||||
>
|
||||
<div>
|
||||
<el-form-item prop="name" label="名称:">
|
||||
<el-input v-model.trim="createFileDialog.name" placeholder="请输入名称" auto-complete="off"></el-input>
|
||||
</el-form-item>
|
||||
<el-form-item prop="type" label="类型:">
|
||||
<el-radio-group v-model="createFileDialog.type">
|
||||
<el-radio label="d" size="small">文件夹</el-radio>
|
||||
<el-radio label="-" size="small">文件</el-radio>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
</div>
|
||||
|
||||
<template #footer>
|
||||
<div class="dialog-footer">
|
||||
<el-button @click="closeCreateFileDialog">关闭</el-button>
|
||||
<el-button v-auth="'machine:file:write'" type="primary" @click="createFile">确定</el-button>
|
||||
</div>
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
<el-dialog
|
||||
:destroy-on-close="true"
|
||||
:title="fileContent.dialogTitle"
|
||||
@@ -164,6 +199,7 @@ import { codemirror } from '@/components/codemirror';
|
||||
import { getSession } from '@/common/utils/storage';
|
||||
import enums from './enums';
|
||||
import config from '@/common/config';
|
||||
import { isTrue } from '@/common/assert';
|
||||
|
||||
export default defineComponent({
|
||||
name: 'FileManage',
|
||||
@@ -184,26 +220,8 @@ export default defineComponent({
|
||||
const fileTree: any = ref(null);
|
||||
const token = getSession('token');
|
||||
|
||||
const cmOptions = {
|
||||
tabSize: 2,
|
||||
mode: 'text/x-sh',
|
||||
theme: 'panda-syntax',
|
||||
line: true,
|
||||
// 开启校验
|
||||
lint: true,
|
||||
gutters: ['CodeMirror-lint-markers'],
|
||||
indentWithTabs: true,
|
||||
smartIndent: true,
|
||||
matchBrackets: true,
|
||||
autofocus: true,
|
||||
styleSelectedText: true,
|
||||
styleActiveLine: true, // 高亮选中行
|
||||
foldGutter: true, // 块槽
|
||||
hintOptions: {
|
||||
// 当匹配只有一项的时候是否自动补全
|
||||
completeSingle: true,
|
||||
},
|
||||
};
|
||||
const folderType = 'd';
|
||||
const fileType = '-';
|
||||
|
||||
const state = reactive({
|
||||
dialogVisible: false,
|
||||
@@ -250,6 +268,12 @@ export default defineComponent({
|
||||
path: '',
|
||||
type: '',
|
||||
},
|
||||
createFileDialog: {
|
||||
visible: false,
|
||||
name: '',
|
||||
type: folderType,
|
||||
node: null as any,
|
||||
},
|
||||
file: null as any,
|
||||
});
|
||||
|
||||
@@ -272,18 +296,6 @@ export default defineComponent({
|
||||
getFiles();
|
||||
};
|
||||
|
||||
/**
|
||||
* tab切换触发事件
|
||||
* @param {Object} tab
|
||||
* @param {Object} event
|
||||
*/
|
||||
// handleClick(tab, event) {
|
||||
// // if (tab.name == 'file-manage') {
|
||||
// // this.fileManage.node.childNodes = [];
|
||||
// // this.loadNode(this.fileManage.node, this.fileManage.resolve);
|
||||
// // }
|
||||
// }
|
||||
|
||||
const add = () => {
|
||||
// 往数组头部添加元素
|
||||
state.fileTable = [{}].concat(state.fileTable);
|
||||
@@ -311,7 +323,6 @@ export default defineComponent({
|
||||
})
|
||||
.then(() => {
|
||||
getFiles();
|
||||
// state.fileTable.splice(idx, 1);
|
||||
});
|
||||
});
|
||||
} else {
|
||||
@@ -388,7 +399,6 @@ export default defineComponent({
|
||||
emit('update:visible', false);
|
||||
emit('update:machineId', null);
|
||||
emit('cancel');
|
||||
// state.activeName = 'conf-file'
|
||||
state.fileTable = [];
|
||||
state.tree.folder = { id: 0 };
|
||||
};
|
||||
@@ -413,7 +423,7 @@ export default defineComponent({
|
||||
return resolve([
|
||||
{
|
||||
name: path,
|
||||
type: 'd',
|
||||
type: folderType,
|
||||
path: path,
|
||||
},
|
||||
]);
|
||||
@@ -435,13 +445,42 @@ export default defineComponent({
|
||||
});
|
||||
for (const file of res) {
|
||||
const type = file.type;
|
||||
if (type != 'd') {
|
||||
if (type == fileType) {
|
||||
file.leaf = true;
|
||||
}
|
||||
}
|
||||
return resolve(res);
|
||||
};
|
||||
|
||||
const showCreateFileDialog = (node: any) => {
|
||||
isTrue(node.expanded, '请先点击展开该节点后再创建');
|
||||
state.createFileDialog.node = node;
|
||||
state.createFileDialog.visible = true;
|
||||
};
|
||||
|
||||
const createFile = async () => {
|
||||
const node = state.createFileDialog.node;
|
||||
console.log(node.data);
|
||||
const name = state.createFileDialog.name;
|
||||
const type = state.createFileDialog.type;
|
||||
const path = node.data.path + '/' + name;
|
||||
await machineApi.createFile.request({
|
||||
machineId: props.machineId,
|
||||
id: state.tree.folder.id,
|
||||
path,
|
||||
type,
|
||||
});
|
||||
fileTree.value.append({ name: name, path: path, type: type, leaf: type === fileType, size: 0 }, node);
|
||||
closeCreateFileDialog();
|
||||
};
|
||||
|
||||
const closeCreateFileDialog = () => {
|
||||
state.createFileDialog.visible = false;
|
||||
state.createFileDialog.node = null;
|
||||
state.createFileDialog.name = '';
|
||||
state.createFileDialog.type = folderType;
|
||||
};
|
||||
|
||||
const deleteFile = (node: any, data: any) => {
|
||||
const file = data.path;
|
||||
ElMessageBox.confirm(`此操作将删除 [${file}], 是否继续?`, '提示', {
|
||||
@@ -468,7 +507,6 @@ export default defineComponent({
|
||||
|
||||
const downloadFile = (node: any, data: any) => {
|
||||
const a = document.createElement('a');
|
||||
// a.setAttribute('target', '_blank')
|
||||
a.setAttribute(
|
||||
'href',
|
||||
`${config.baseApiUrl}/machines/${props.machineId}/files/${state.tree.folder.id}/read?type=1&path=${data.path}&token=${token}`
|
||||
@@ -516,7 +554,6 @@ export default defineComponent({
|
||||
|
||||
const beforeUpload = (file: File) => {
|
||||
state.file = file;
|
||||
// ElMessage.success(`'${file.name}' 上传中,请关注结果通知`);
|
||||
};
|
||||
const getFilePath = (data: object, visible: boolean) => {
|
||||
if (visible) {
|
||||
@@ -572,7 +609,6 @@ export default defineComponent({
|
||||
fileTree,
|
||||
enums,
|
||||
token,
|
||||
cmOptions,
|
||||
add,
|
||||
getFiles,
|
||||
handlePageChange,
|
||||
@@ -583,6 +619,9 @@ export default defineComponent({
|
||||
updateContent,
|
||||
handleClose,
|
||||
loadNode,
|
||||
showCreateFileDialog,
|
||||
closeCreateFileDialog,
|
||||
createFile,
|
||||
deleteFile,
|
||||
downloadFile,
|
||||
getUploadFile,
|
||||
|
||||
@@ -25,6 +25,7 @@ export const machineApi = {
|
||||
rmFile: Api.create("/machines/{machineId}/files/{fileId}/remove", 'delete'),
|
||||
uploadFile: Api.create("/machines/{machineId}/files/{fileId}/upload?token={token}", 'post'),
|
||||
fileContent: Api.create("/machines/{machineId}/files/{fileId}/read", 'get'),
|
||||
createFile: Api.create("/machines/{machineId}/files/{id}/create-file", 'post'),
|
||||
// 修改文件内容
|
||||
updateFileContent: Api.create("/machines/{machineId}/files/{id}/write", 'post'),
|
||||
// 添加文件or目录
|
||||
|
||||
@@ -250,7 +250,7 @@ export default defineComponent({
|
||||
|
||||
onMounted(async () => {
|
||||
search();
|
||||
state.projects = (await projectApi.projects.request({ pageNum: 1, pageSize: 100 })).list;
|
||||
state.projects = await projectApi.accountProjects.request(null);
|
||||
});
|
||||
|
||||
const handlePageChange = (curPage: number) => {
|
||||
@@ -266,12 +266,6 @@ export default defineComponent({
|
||||
state.currentData = item;
|
||||
};
|
||||
|
||||
// connect() {
|
||||
// Req.post('/open/redis/connect', this.form, res => {
|
||||
// this.redisInfo = res
|
||||
// })
|
||||
// }
|
||||
|
||||
const showDatabases = async (id: number) => {
|
||||
state.databaseDialog.data = (await mongoApi.databases.request({ id })).Databases;
|
||||
state.databaseDialog.title = `数据库列表`;
|
||||
@@ -371,14 +365,6 @@ export default defineComponent({
|
||||
} catch (err) {}
|
||||
};
|
||||
|
||||
// const info = (redis: any) => {
|
||||
// redisApi.redisInfo.request({ id: redis.id }).then((res: any) => {
|
||||
// state.infoDialog.info = res;
|
||||
// state.infoDialog.title = `'${redis.host}' info`;
|
||||
// state.infoDialog.visible = true;
|
||||
// });
|
||||
// };
|
||||
|
||||
const search = async () => {
|
||||
const res = await mongoApi.mongoList.request(state.query);
|
||||
state.list = res.list;
|
||||
|
||||
@@ -118,7 +118,7 @@ export default defineComponent({
|
||||
|
||||
onMounted(async () => {
|
||||
search();
|
||||
state.projects = (await projectApi.projects.request({ pageNum: 1, pageSize: 100 })).list;
|
||||
state.projects = await projectApi.accountProjects.request(null);
|
||||
});
|
||||
|
||||
const handlePageChange = (curPage: number) => {
|
||||
|
||||
@@ -15,6 +15,13 @@ type DbForm struct {
|
||||
EnvId uint64 `binding:"required" json:"envId"`
|
||||
}
|
||||
|
||||
type DbSqlSaveForm struct {
|
||||
Name string
|
||||
Sql string `binding:"required"`
|
||||
Type int `binding:"required"`
|
||||
Db string `binding:"required"`
|
||||
}
|
||||
|
||||
// 数据库SQL执行表单
|
||||
type DbSqlExecForm struct {
|
||||
Db string `binding:"required" json:"db"` //数据库名
|
||||
|
||||
@@ -38,11 +38,9 @@ type MachineScriptForm struct {
|
||||
Script string `binding:"required"`
|
||||
}
|
||||
|
||||
type DbSqlSaveForm struct {
|
||||
Name string
|
||||
Sql string `binding:"required"`
|
||||
Type int `binding:"required"`
|
||||
Db string `binding:"required"`
|
||||
type MachineCreateFileForm struct {
|
||||
Path string `binding:"required"`
|
||||
Type string `binding:"required"`
|
||||
}
|
||||
|
||||
type MachineFileUpdateForm struct {
|
||||
@@ -60,6 +60,22 @@ func (m *MachineFile) DeleteFile(rc *ctx.ReqCtx) {
|
||||
|
||||
/*** sftp相关操作 */
|
||||
|
||||
func (m *MachineFile) CreateFile(rc *ctx.ReqCtx) {
|
||||
g := rc.GinCtx
|
||||
fid := GetMachineFileId(g)
|
||||
|
||||
form := new(form.MachineCreateFileForm)
|
||||
ginx.BindJsonAndValid(g, form)
|
||||
path := form.Path
|
||||
|
||||
if form.Type == dir {
|
||||
m.MachineFileApp.MkDir(fid, form.Path)
|
||||
} else {
|
||||
m.MachineFileApp.CreateFile(fid, form.Path)
|
||||
}
|
||||
rc.ReqParam = fmt.Sprintf("path: %s, type: %s", path, form.Type)
|
||||
}
|
||||
|
||||
func (m *MachineFile) ReadFileContent(rc *ctx.ReqCtx) {
|
||||
g := rc.GinCtx
|
||||
fid := GetMachineFileId(g)
|
||||
@@ -104,6 +120,7 @@ func (m *MachineFile) GetDirEntry(rc *ctx.ReqCtx) {
|
||||
Size: fi.Size(),
|
||||
Path: readPath + fi.Name(),
|
||||
Type: getFileType(fi.Mode()),
|
||||
Mode: fi.Mode().String(),
|
||||
})
|
||||
}
|
||||
rc.ResData = fisVO
|
||||
@@ -155,7 +172,6 @@ func (m *MachineFile) UploadFile(rc *ctx.ReqCtx) {
|
||||
func (m *MachineFile) RemoveFile(rc *ctx.ReqCtx) {
|
||||
g := rc.GinCtx
|
||||
fid := GetMachineFileId(g)
|
||||
// mid := GetMachineId(g)
|
||||
path := g.Query("path")
|
||||
|
||||
m.MachineFileApp.RemoveFile(fid, path)
|
||||
@@ -167,7 +183,10 @@ func getFileType(fm fs.FileMode) string {
|
||||
if fm.IsDir() {
|
||||
return dir
|
||||
}
|
||||
return file
|
||||
if fm.IsRegular() {
|
||||
return file
|
||||
}
|
||||
return dir
|
||||
}
|
||||
|
||||
func GetMachineFileId(g *gin.Context) uint64 {
|
||||
|
||||
@@ -56,6 +56,7 @@ type MachineFileInfo struct {
|
||||
Path string `json:"path"`
|
||||
Size int64 `json:"size"`
|
||||
Type string `json:"type"`
|
||||
Mode string `json:"mode"`
|
||||
}
|
||||
|
||||
type RoleVO struct {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package application
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"io/fs"
|
||||
"mayfly-go/internal/devops/domain/entity"
|
||||
@@ -30,6 +31,12 @@ type MachineFile interface {
|
||||
|
||||
/** sftp 相关操作 **/
|
||||
|
||||
// 创建目录
|
||||
MkDir(fid uint64, path string)
|
||||
|
||||
// 创建文件
|
||||
CreateFile(fid uint64, path string)
|
||||
|
||||
// 读取目录
|
||||
ReadDir(fid uint64, path string) []fs.FileInfo
|
||||
|
||||
@@ -100,6 +107,25 @@ func (m *machineFileAppImpl) ReadDir(fid uint64, path string) []fs.FileInfo {
|
||||
return fis
|
||||
}
|
||||
|
||||
func (m *machineFileAppImpl) MkDir(fid uint64, path string) {
|
||||
path, machineId := m.checkAndReturnPathMid(fid, path)
|
||||
if !strings.HasSuffix(path, "/") {
|
||||
path = path + "/"
|
||||
}
|
||||
|
||||
sftpCli := m.getSftpCli(machineId)
|
||||
err := sftpCli.Mkdir(path)
|
||||
biz.ErrIsNilAppendErr(err, "创建目录失败: %s")
|
||||
}
|
||||
|
||||
func (m *machineFileAppImpl) CreateFile(fid uint64, path string) {
|
||||
path, machineId := m.checkAndReturnPathMid(fid, path)
|
||||
sftpCli := m.getSftpCli(machineId)
|
||||
file, err := sftpCli.Create(path)
|
||||
biz.ErrIsNilAppendErr(err, "创建文件失败: %s")
|
||||
defer file.Close()
|
||||
}
|
||||
|
||||
func (m *machineFileAppImpl) ReadFile(fileId uint64, path string) *sftp.File {
|
||||
path, machineId := m.checkAndReturnPathMid(fileId, path)
|
||||
sftpCli := m.getSftpCli(machineId)
|
||||
@@ -148,6 +174,11 @@ func (m *machineFileAppImpl) RemoveFile(fileId uint64, path string) {
|
||||
fi, _ := file.Stat()
|
||||
if fi.IsDir() {
|
||||
err = sftpCli.RemoveDirectory(path)
|
||||
// 如果文件夹有内容会删除失败,则使用rm -rf命令删除
|
||||
if err != nil {
|
||||
MachineApp.GetCli(machineId).Run(fmt.Sprintf("rm -rf %s", path))
|
||||
err = nil
|
||||
}
|
||||
} else {
|
||||
err = sftpCli.Remove(path)
|
||||
}
|
||||
|
||||
@@ -43,14 +43,14 @@ func InitMachineFileRouter(router *gin.RouterGroup) {
|
||||
|
||||
getContent := ctx.NewLogInfo("读取机器文件内容")
|
||||
machineFile.GET(":machineId/files/:fileId/read", func(c *gin.Context) {
|
||||
rc := ctx.NewReqCtxWithGin(c).WithLog(getContent)
|
||||
rc.Handle(mf.ReadFileContent)
|
||||
ctx.NewReqCtxWithGin(c).WithLog(getContent).
|
||||
Handle(mf.ReadFileContent)
|
||||
})
|
||||
|
||||
getDir := ctx.NewLogInfo("读取机器目录")
|
||||
machineFile.GET(":machineId/files/:fileId/read-dir", func(c *gin.Context) {
|
||||
rc := ctx.NewReqCtxWithGin(c).WithLog(getDir)
|
||||
rc.Handle(mf.GetDirEntry)
|
||||
ctx.NewReqCtxWithGin(c).WithLog(getDir).
|
||||
Handle(mf.GetDirEntry)
|
||||
})
|
||||
|
||||
writeFile := ctx.NewLogInfo("写入or下载文件内容")
|
||||
@@ -61,6 +61,13 @@ func InitMachineFileRouter(router *gin.RouterGroup) {
|
||||
Handle(mf.WriteFileContent)
|
||||
})
|
||||
|
||||
createFile := ctx.NewLogInfo("创建机器文件or目录")
|
||||
machineFile.POST(":machineId/files/:fileId/create-file", func(c *gin.Context) {
|
||||
ctx.NewReqCtxWithGin(c).WithLog(createFile).
|
||||
WithRequiredPermission(wfP).
|
||||
Handle(mf.CreateFile)
|
||||
})
|
||||
|
||||
uploadFile := ctx.NewLogInfo("文件上传")
|
||||
ufP := ctx.NewPermission("machine:file:upload")
|
||||
machineFile.POST(":machineId/files/:fileId/upload", func(c *gin.Context) {
|
||||
|
||||
Reference in New Issue
Block a user