vue ssh 修改目录、ssh 文件管理支持上传压缩包

This commit is contained in:
bwcx_jzy 2021-12-25 19:34:30 +08:00
parent 83ccc4d28e
commit 2ecd9a167a
No known key found for this signature in database
GPG Key ID: 5E48E9372088B9E5
18 changed files with 105 additions and 79 deletions

View File

@ -5,10 +5,12 @@
### 新增功能
1. 脚本模版新增日志管理
2. 【server】ssh 文件管理新增导入压缩包自动解压(感谢@刘志远)
### 解决BUG、优化功能
1. 【server】节点分发数据新增状态字段,启动程序时候触发修护异常数据
2. 【server】定时执行相关 cron 表达式输入提示示例数据
------

View File

@ -33,11 +33,7 @@
<artifactId>bcprov-jdk15on</artifactId>
<version>1.69</version>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-compress</artifactId>
<version>1.21</version>
</dependency>
<dependency>
<groupId>io.jpom.plugins</groupId>
<artifactId>webhook</artifactId>

View File

@ -44,6 +44,12 @@
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-compress</artifactId>
<version>1.21</version>
</dependency>
</dependencies>
<profiles>

View File

@ -1,6 +1,7 @@
package io.jpom.controller.node.ssh;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.convert.Convert;
import cn.hutool.core.date.DatePattern;
import cn.hutool.core.date.DateUtil;
import cn.hutool.core.io.FileUtil;
@ -26,6 +27,9 @@ import io.jpom.plugin.Feature;
import io.jpom.plugin.MethodFeature;
import io.jpom.service.node.ssh.SshService;
import io.jpom.system.ServerConfigBean;
import io.jpom.util.CommandUtil;
import io.jpom.util.CompressionFileUtil;
import io.jpom.util.StringUtil;
import org.springframework.http.MediaType;
import org.springframework.util.Assert;
import org.springframework.web.bind.annotation.RequestMapping;
@ -122,7 +126,6 @@ public class SshFileController extends BaseServerController {
}
@RequestMapping(value = "list_file_data.json", method = RequestMethod.POST, produces = MediaType.APPLICATION_JSON_VALUE)
@ResponseBody
@Feature(method = MethodFeature.LIST)
public String listData(String id, String path, String children) throws SftpException {
SshModel sshModel = this.check(id, path, children);
@ -132,7 +135,6 @@ public class SshFileController extends BaseServerController {
}
@RequestMapping(value = "read_file_data.json", method = RequestMethod.POST, produces = MediaType.APPLICATION_JSON_VALUE)
@ResponseBody
@Feature(method = MethodFeature.LIST)
public String readFileData(String id, String path, String children) {
SshModel sshModel = this.check(id, path, children);
@ -145,7 +147,6 @@ public class SshFileController extends BaseServerController {
}
@RequestMapping(value = "update_file_data.json", method = RequestMethod.POST, produces = MediaType.APPLICATION_JSON_VALUE)
@ResponseBody
@Feature(method = MethodFeature.EDIT)
public String updateFileData(String id, String path, String children, String content) {
SshModel sshModel = this.check(id, path, children);
@ -413,23 +414,48 @@ public class SshFileController extends BaseServerController {
@RequestMapping(value = "upload", method = RequestMethod.POST, produces = MediaType.APPLICATION_JSON_VALUE)
@Feature(method = MethodFeature.UPLOAD)
public String upload(String id, String path, String name) {
public String upload(String id, String path, String name, String unzip) {
SshModel sshModel = sshService.getByKey(id, false);
Assert.notNull(sshModel, "ssh error");
List<String> fileDirs = sshModel.fileDirs();
Assert.state(CollUtil.contains(fileDirs, path), "没有配置此文件夹");
String remotePath = FileUtil.normalize(path + StrUtil.SLASH + name);
Session session = null;
ChannelSftp channel = null;
String localPath = null;
try {
session = SshService.getSessionByModel(sshModel);
channel = (ChannelSftp) JschUtil.openChannel(session, ChannelType.SFTP);
MultipartFileBuilder multipartFileBuilder = createMultipart().addFieldName("file").setUseOriginalFilename(true);
localPath = multipartFileBuilder.save();
File file = FileUtil.file(localPath);
String normalize = FileUtil.normalize(path + StrUtil.SLASH + name);
channel.cd(normalize);
channel.put(IoUtil.toStream(file), file.getName());
MultipartFileBuilder multipart = createMultipart();
// 保存路径
File tempPath = ServerConfigBean.getInstance().getUserTempPath();
File savePath = FileUtil.file(tempPath, "ssh", sshModel.getId());
multipart.setSavePath(FileUtil.getAbsolutePath(savePath));
multipart.addFieldName("file")
.setUseOriginalFilename(true);
//
if (Convert.toBool(unzip, false)) {
multipart.setFileExt(StringUtil.PACKAGE_EXT);
localPath = multipart.save();
// 解压
File file = new File(localPath);
File tempUnzipPath = FileUtil.file(savePath, IdUtil.fastSimpleUUID());
try {
CompressionFileUtil.unCompress(file, tempUnzipPath);
// 同步上传文件
sshService.uploadDir(sshModel, remotePath, tempUnzipPath);
} finally {
// 删除临时文件
CommandUtil.systemFastDel(file);
CommandUtil.systemFastDel(tempUnzipPath);
}
} else {
localPath = multipart.save();
File file = FileUtil.file(localPath);
channel.cd(remotePath);
channel.put(IoUtil.toStream(file), file.getName());
}
} catch (Exception e) {
DefaultSystemLog.getLog().error("ssh上传文件异常", e);
return JsonMessage.getString(400, "上传失败");

View File

@ -181,7 +181,7 @@ export default {
},
};
</script>
<style>
<style scoped>
#app-layout {
min-height: 100vh;
}

View File

@ -183,7 +183,7 @@ import { getNodeList, getNodeStatus, editNode, deleteNode, syncProject, unLockWo
import { getSshListAll } from "@/api/ssh";
import { syncScript } from "@/api/node-other";
import NodeLayout from "./node-layout";
import Terminal from "./terminal";
import Terminal from "@/pages/ssh/terminal";
import { parseTime } from "@/utils/time";
import { PAGE_DEFAULT_LIMIT, PAGE_DEFAULT_SIZW_OPTIONS, PAGE_DEFAULT_SHOW_TOTAL, PAGE_DEFAULT_LIST_QUERY } from "@/utils/const";
import { getWorkSpaceListAll } from "@/api/workspace";

View File

@ -63,6 +63,8 @@
</a-drawer>
<!-- 上传文件 -->
<a-modal v-model="uploadFileVisible" width="300px" title="上传 bat|bash 文件" :footer="null" :maskClosable="true">
<a-alert message=" 导入文件将自动安装文件名去重、如果已经存在的将自动覆盖内容" banner />
<br />
<a-upload :file-list="uploadFileList" :remove="handleRemove" :before-upload="beforeUpload" :accept="'.bat,.sh'">
<a-button><a-icon type="upload" />选择 bat|bash 文件</a-button>
</a-upload>

View File

@ -72,7 +72,7 @@
</a-modal>
<!-- 上传压缩文件 -->
<a-modal v-model="uploadZipFileVisible" width="300px" title="上传压缩文件" :footer="null" :maskClosable="true">
<a-upload :file-list="uploadFileList" :remove="handleZipRemove" :before-upload="beforeZipUpload" accept=".tar,.bz2,.gz,.zip,.tar.bz2,.tar.gz">
<a-upload :file-list="uploadFileList" :remove="handleZipRemove" :before-upload="beforeZipUpload" :accept="ZIP_ACCEPT">
<a-button><a-icon type="upload" />选择压缩文件</a-button>
</a-upload>
<br />
@ -105,7 +105,7 @@
</template>
<script>
import { getFileList, downloadProjectFile, deleteProjectFile, uploadProjectFile, readFile, updateFile, remoteDownload } from "../../../../api/node-project";
import { ZIP_ACCEPT } from "@/utils/const";
import codeEditor from "@/components/codeEditor";
export default {
@ -132,6 +132,7 @@ export default {
},
data() {
return {
ZIP_ACCEPT: ZIP_ACCEPT,
loading: false,
treeList: [],
fileList: [],
@ -311,7 +312,6 @@ export default {
if (Object.keys(this.tempNode).length === 0) {
this.$notification.error({
message: "请选择一个节点",
});
return;
}
@ -334,7 +334,6 @@ export default {
startUpload() {
this.$notification.info({
message: "正在上传文件,请稍后...",
});
//
this.uploading = true;
@ -356,7 +355,6 @@ export default {
if (res.code === 200) {
this.$notification.success({
message: res.msg,
});
this.successSize++;
}
@ -379,7 +377,6 @@ export default {
if (Object.keys(this.tempNode).length === 0) {
this.$notification.error({
message: "请选择一个节点",
});
return;
}
@ -398,7 +395,6 @@ export default {
startZipUpload() {
this.$notification.info({
message: "正在上传文件,请稍后...",
});
//
this.uploading = true;
@ -421,7 +417,6 @@ export default {
if (res.code === 200) {
this.$notification.success({
message: res.msg,
});
this.successSize++;
this.percentage = 100;
@ -461,7 +456,6 @@ export default {
if (res.code == 200) {
this.$notification.success({
message: res.msg,
});
this.remoteDownloadData = {};
this.uploadRemoteFileVisible = false;
@ -478,7 +472,6 @@ export default {
if (Object.keys(this.tempNode).length === 0) {
this.$notification.warn({
message: "请选择一个节点",
});
return false;
}
@ -525,7 +518,6 @@ export default {
if (res.code === 200) {
this.$notification.success({
message: res.msg,
});
this.loadFileList();
}
@ -537,7 +529,6 @@ export default {
handleDownload(record) {
this.$notification.info({
message: "正在下载,请稍等...",
});
//
const params = {
@ -577,7 +568,6 @@ export default {
if (res.code === 200) {
this.$notification.success({
message: res.msg,
});
this.loadData();
this.loadFileList();

View File

@ -1,6 +1,6 @@
<template>
<!-- 布局 -->
<a-layout class="file-layout">
<a-layout class="ssh-file-layout">
<!-- 目录树 -->
<a-layout-sider theme="light" class="sider" width="25%">
<a-empty v-if="treeList.length === 0" />
@ -10,21 +10,13 @@
<a-layout-content class="file-content">
<div ref="filter" class="filter">
<a-button :disabled="!this.tempNode.parentDir" type="primary" @click="handleUpload">上传文件</a-button>
<a-button :disabled="!this.tempNode.parentDir" type="primary" @click="handleUploadZip">上传压缩文件自动解压</a-button>
<a-button :disabled="!this.tempNode.parentDir" type="primary" @click="loadFileList()">刷新</a-button>
<a-button :disabled="!this.tempNode.parentDir" type="danger" @click="handleDeletePath()">删除</a-button>
<span v-if="this.tempNode.parentDir">当前目录:{{ this.tempNode.path }}</span
><span v-if="this.tempNode.parentDir">{{ this.tempNode.parentDir }}</span>
</div>
<a-table
:data-source="fileList"
:loading="loading"
:columns="columns"
:pagination="false"
bordered
:style="{ 'max-height': tableHeight + 'px' }"
:scroll="{ x: 790, y: tableHeight - 60 }"
:rowKey="(record, index) => index"
>
<a-table :data-source="fileList" :loading="loading" :columns="columns" :pagination="false" bordered :rowKey="(record, index) => index">
<a-tooltip slot="name" slot-scope="text" placement="topLeft" :title="text">
<span>{{ text }}</span>
</a-tooltip>
@ -45,8 +37,11 @@
</a-table>
<!-- 上传文件 -->
<a-modal v-model="uploadFileVisible" width="300px" title="上传文件" :footer="null" :maskClosable="true">
<a-upload :file-list="uploadFileList" :remove="handleRemove" :before-upload="beforeUpload" multiple>
<a-button><a-icon type="upload" />选择文件</a-button>
<a-upload :file-list="uploadFileList" :remove="handleRemove" :before-upload="beforeUpload" :accept="`${uploadFileZip ? ZIP_ACCEPT : ''}`" :multiple="!uploadFileZip">
<a-button>
<a-icon type="upload" />选择文件
{{ uploadFileZip ? "压缩包" : "" }}
</a-button>
</a-upload>
<br />
<a-button type="primary" :disabled="uploadFileList.length === 0" @click="startUpload">开始上传</a-button>
@ -67,6 +62,7 @@
import { getRootFileList, getFileList, downloadFile, deleteFile, uploadFile, readFile, updateFileData } from "@/api/ssh";
import Terminal from "./terminal";
import codeEditor from "@/components/codeEditor";
import { ZIP_ACCEPT } from "@/utils/const";
export default {
props: {
ssh: {
@ -86,6 +82,8 @@ export default {
tempNode: {},
temp: {},
uploadFileVisible: false,
uploadFileZip: false,
ZIP_ACCEPT: ZIP_ACCEPT,
terminalVisible: false,
tableHeight: "80vh",
replaceFields: {
@ -138,11 +136,15 @@ export default {
if (Object.keys(this.tempNode).length === 0) {
this.$notification.error({
message: "请选择一个节点",
});
return;
}
this.uploadFileVisible = true;
this.uploadFileZip = false;
},
handleUploadZip() {
this.handleUpload();
this.uploadFileZip = true;
},
handleRemove(file) {
const index = this.uploadFileList.indexOf(file);
@ -161,17 +163,15 @@ export default {
formData.append("file", file);
formData.append("id", this.ssh.id);
formData.append("name", this.tempNode.parentDir);
formData.append("unzip", this.uploadFileZip);
formData.append("path", this.tempNode.path);
//
uploadFile(formData).then((res) => {
if (res.code === 200) {
this.$notification.success({
message: res.msg,
});
this.loadFileList();
this.uploadFileList = [];
this.uploadFileVisible = false;
}
@ -232,7 +232,6 @@ export default {
if (Object.keys(this.tempNode).length === 0) {
this.$notification.warn({
message: "请选择一个节点",
});
return false;
}
@ -248,15 +247,17 @@ export default {
getFileList(params).then((res) => {
if (res.code === 200) {
//
res.data.forEach((element) => {
if (!element.dir) {
this.fileList = res.data
.filter((element) => {
return !element.dir;
})
.map((element) => {
//
this.fileList.push({
return {
path: this.tempNode.path,
...element,
});
}
});
};
});
}
this.loading = false;
});
@ -287,7 +288,6 @@ export default {
updateFileData(params).then((res) => {
this.$notification.success({
message: res.msg,
});
if (res.code == 200) {
this.editFileVisible = false;
@ -337,7 +337,6 @@ export default {
if (res.code === 200) {
this.$notification.success({
message: res.msg,
});
//
this.loadData();
@ -365,7 +364,6 @@ export default {
if (res.code === 200) {
this.$notification.success({
message: res.msg,
});
this.loadFileList();
}
@ -376,18 +374,19 @@ export default {
},
};
</script>
<style scoped>
.file-layout {
<style scoped lang="stylus">
.ssh-file-layout {
padding: 0;
min-height calc(100vh - 75px);
}
.sider {
border: 1px solid #e2e2e2;
height: calc(100vh - 80px);
overflow-y: auto;
/* height: calc(100vh - 80px); */
/* overflow-y: auto; */
}
.file-content {
height: calc(100vh - 100px);
overflow-y: auto;
/* height: calc(100vh - 100px); */
/* overflow-y: auto; */
margin: 10px 10px 0;
padding: 10px;
background-color: #fff;

View File

@ -232,8 +232,8 @@
</template>
<script>
import { deleteSsh, editSsh, getSshList, getSshCheckAgent, getSshOperationLogList, installAgentNode } from "@/api/ssh";
import SshFile from "./ssh-file";
import Terminal from "./terminal";
import SshFile from "@/pages/ssh/ssh-file";
import Terminal from "@/pages/ssh/terminal";
import { parseTime } from "@/utils/time";
import { PAGE_DEFAULT_LIMIT, PAGE_DEFAULT_SIZW_OPTIONS, PAGE_DEFAULT_SHOW_TOTAL, PAGE_DEFAULT_LIST_QUERY } from "@/utils/const";

View File

@ -21,11 +21,6 @@ const children = [
name: "node-list",
component: () => import("../pages/node/list"),
},
{
path: "/node/ssh",
name: "node-ssh",
component: () => import("../pages/node/ssh"),
},
{
path: "/node/update",
name: "node-update",
@ -42,14 +37,19 @@ const children = [
component: () => import("../pages/node/script-list"),
},
{
path: "/node/ssh/command",
name: "node-command",
component: () => import("../pages/node/ssh/command"),
path: "/ssh",
name: "node-ssh",
component: () => import("../pages/ssh/ssh"),
},
{
path: "/node/ssh/command-log",
path: "/ssh/command",
name: "node-command",
component: () => import("../pages/node/ssh/command-log"),
component: () => import("../pages/ssh/command"),
},
{
path: "/ssh/command-log",
name: "node-command",
component: () => import("../pages/ssh/command-log"),
},
{
path: "/dispatch/list",

View File

@ -5,9 +5,9 @@
*/
const routeMenuMap = {
nodeList: "/node/list",
sshList: "/node/ssh",
commandList: "/node/ssh/command",
commandLogList: "/node/ssh/command-log",
sshList: "/ssh",
commandList: "/ssh/command",
commandLogList: "/ssh/command-log",
nodeUpdate: "/node/update",
outgiving: "/dispatch/list",
outgivingLog: "/dispatch/log",

View File

@ -62,7 +62,7 @@ export const RESTART_UPGRADE_WAIT_TIME_COUNT = 80;
/**
* 定时 cron 默认提示
*
*
* https://www.npmjs.com/package/cron-parser
*/
export const CRON_DATA_SOURCE = [
@ -119,3 +119,8 @@ export const CRON_DATA_SOURCE = [
],
},
];
/**
* 压缩文件格式
*/
export const ZIP_ACCEPT = ".tar,.bz2,.gz,.zip,.tar.bz2,.tar.gz";