删除文件.

This commit is contained in:
lijiahangmax
2024-02-20 00:06:10 +08:00
parent a2ff7b0076
commit bde002ee34
21 changed files with 518 additions and 130 deletions

View File

@@ -79,4 +79,6 @@ public interface ErrorMessage {
String PATH_NOT_NORMALIZE = "路径不合法"; String PATH_NOT_NORMALIZE = "路径不合法";
String OPERATE_ERROR = "操作失败";
} }

View File

@@ -104,8 +104,8 @@ public enum InputTypeEnum {
*/ */
SFTP_REMOVE("rm", SFTP_REMOVE("rm",
SftpRemoveHandler.class, SftpRemoveHandler.class,
new String[]{"type", "sessionId", "paths"}, new String[]{"type", "sessionId", "path"},
SftpRemoveRequest.class), SftpBaseRequest.class),
/** /**
* SFTP 截断文件 * SFTP 截断文件

View File

@@ -48,12 +48,12 @@ public enum OutputTypeEnum {
/** /**
* SFTP 创建文件夹 * SFTP 创建文件夹
*/ */
SFTP_MKDIR("md", "${type}|${sessionId}|${result}|${msg}"), SFTP_MKDIR("mk", "${type}|${sessionId}|${result}|${msg}"),
/** /**
* SFTP 创建文件 * SFTP 创建文件
*/ */
SFTP_TOUCH("to", "${type}|${sessionId}${result}|${msg}"), SFTP_TOUCH("to", "${type}|${sessionId}|${result}|${msg}"),
/** /**
* SFTP 移动文件 * SFTP 移动文件

View File

@@ -2,7 +2,7 @@ package com.orion.ops.module.asset.handler.host.terminal.handler;
import com.orion.ops.framework.common.enums.BooleanBit; import com.orion.ops.framework.common.enums.BooleanBit;
import com.orion.ops.module.asset.handler.host.terminal.enums.OutputTypeEnum; import com.orion.ops.module.asset.handler.host.terminal.enums.OutputTypeEnum;
import com.orion.ops.module.asset.handler.host.terminal.model.request.SftpRemoveRequest; import com.orion.ops.module.asset.handler.host.terminal.model.request.SftpBaseRequest;
import com.orion.ops.module.asset.handler.host.terminal.model.response.SftpBaseResponse; import com.orion.ops.module.asset.handler.host.terminal.model.response.SftpBaseResponse;
import com.orion.ops.module.asset.handler.host.terminal.session.ISftpSession; import com.orion.ops.module.asset.handler.host.terminal.session.ISftpSession;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
@@ -18,14 +18,14 @@ import org.springframework.web.socket.WebSocketSession;
*/ */
@Slf4j @Slf4j
@Component @Component
public class SftpRemoveHandler extends AbstractTerminalHandler<SftpRemoveRequest> { public class SftpRemoveHandler extends AbstractTerminalHandler<SftpBaseRequest> {
@Override @Override
public void handle(WebSocketSession channel, SftpRemoveRequest payload) { public void handle(WebSocketSession channel, SftpBaseRequest payload) {
// 获取会话 // 获取会话
ISftpSession session = terminalManager.getSession(channel.getId(), payload.getSessionId()); ISftpSession session = terminalManager.getSession(channel.getId(), payload.getSessionId());
String[] paths = payload.getPaths().split("\\|"); String[] paths = payload.getPath().split("\\|");
log.info("SftpRemoveHandler-handle session: {}, paths: {}", payload.getSessionId(), paths); log.info("SftpRemoveHandler-handle session: {}, path: {}", payload.getSessionId(), paths);
Exception ex = null; Exception ex = null;
// 删除 // 删除
try { try {
@@ -40,7 +40,7 @@ public class SftpRemoveHandler extends AbstractTerminalHandler<SftpRemoveRequest
SftpBaseResponse.builder() SftpBaseResponse.builder()
.sessionId(payload.getSessionId()) .sessionId(payload.getSessionId())
.result(BooleanBit.of(ex == null).getValue()) .result(BooleanBit.of(ex == null).getValue())
.msg(ex == null ? null : ex.getMessage()) .msg(this.getErrorMessage(ex))
.build()); .build());
} }

View File

@@ -1,32 +0,0 @@
package com.orion.ops.module.asset.handler.host.terminal.model.request;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.NoArgsConstructor;
import lombok.experimental.SuperBuilder;
import java.util.List;
/**
* sftp 删除文件 实体对象
* <p>
* i|eff00a1|paths
*
* @author Jiahang Li
* @version 1.0.0
* @since 2024/2/6 13:31
*/
@Data
@SuperBuilder
@NoArgsConstructor
@AllArgsConstructor
@EqualsAndHashCode(callSuper = true)
@Schema(name = "SftpRemoveRequest", description = "sftp 删除文件 实体对象")
public class SftpRemoveRequest extends SftpBaseRequest {
@Schema(description = "paths 多个用|分割")
private String paths;
}

View File

@@ -17,6 +17,7 @@ import org.springframework.web.socket.WebSocketSession;
import java.io.InputStream; import java.io.InputStream;
import java.util.Arrays; import java.util.Arrays;
import java.util.Comparator;
import java.util.List; import java.util.List;
import java.util.Optional; import java.util.Optional;
import java.util.stream.Collectors; import java.util.stream.Collectors;
@@ -68,6 +69,7 @@ public class SftpSession extends TerminalSession implements ISftpSession {
true); true);
return files.stream() return files.stream()
.map(SftpSession::fileMapping) .map(SftpSession::fileMapping)
.sorted(Comparator.comparing(SftpFileVO::getName))
.collect(Collectors.toList()); .collect(Collectors.toList());
} }

View File

@@ -5,11 +5,11 @@
// Read more: https://github.com/vuejs/core/pull/3399 // Read more: https://github.com/vuejs/core/pull/3399
import '@vue/runtime-core'; import '@vue/runtime-core';
export {} export {};
declare module '@vue/runtime-core' { declare module '@vue/runtime-core' {
export interface GlobalComponents { export interface GlobalComponents {
RouterLink: typeof import('vue-router')['RouterLink'] RouterLink: typeof import('vue-router')['RouterLink'];
RouterView: typeof import('vue-router')['RouterView'] RouterView: typeof import('vue-router')['RouterView'];
} }
} }

View File

@@ -0,0 +1,9 @@
import { nextTick } from 'vue';
// 设置 ref 自动聚焦
export const setAutoFocus = (el: HTMLElement) => {
// 自动聚焦
nextTick(() => {
el && el.focus();
});
};

View File

@@ -1,5 +1,5 @@
<template> <template>
<a-dropdown class="host-space-context-menu" <a-dropdown class="terminal-context-menu"
:popup-max-height="false" :popup-max-height="false"
trigger="contextMenu" trigger="contextMenu"
position="bl" position="bl"
@@ -53,35 +53,35 @@
<template #content> <template #content>
<!-- 复制 --> <!-- 复制 -->
<a-doption @click="copyCommand"> <a-doption @click="copyCommand">
<div class="host-space-context-menu-icon"> <div class="terminal-context-menu-icon">
<icon-copy /> <icon-copy />
</div> </div>
<div>复制</div> <div>复制</div>
</a-doption> </a-doption>
<!-- 粘贴 --> <!-- 粘贴 -->
<a-doption @click="paste"> <a-doption @click="paste">
<div class="host-space-context-menu-icon"> <div class="terminal-context-menu-icon">
<icon-paste /> <icon-paste />
</div> </div>
<div>粘贴</div> <div>粘贴</div>
</a-doption> </a-doption>
<!-- 执行 --> <!-- 执行 -->
<a-doption @click="exec"> <a-doption @click="exec">
<div class="host-space-context-menu-icon"> <div class="terminal-context-menu-icon">
<icon-thunderbolt /> <icon-thunderbolt />
</div> </div>
<div>执行</div> <div>执行</div>
</a-doption> </a-doption>
<!-- 修改 --> <!-- 修改 -->
<a-doption @click="openUpdateSnippet(item)"> <a-doption @click="openUpdateSnippet(item)">
<div class="host-space-context-menu-icon"> <div class="terminal-context-menu-icon">
<icon-edit /> <icon-edit />
</div> </div>
<div>修改</div> <div>修改</div>
</a-doption> </a-doption>
<!-- 删除 --> <!-- 删除 -->
<a-doption @click="removeSnippet(item.id)"> <a-doption @click="removeSnippet(item.id)">
<div class="host-space-context-menu-icon"> <div class="terminal-context-menu-icon">
<icon-delete /> <icon-delete />
</div> </div>
<div>删除</div> <div>删除</div>
@@ -89,7 +89,7 @@
<!-- 展开 --> <!-- 展开 -->
<a-doption v-if="!item.expand" <a-doption v-if="!item.expand"
@click="() => item.expand = true"> @click="() => item.expand = true">
<div class="host-space-context-menu-icon"> <div class="terminal-context-menu-icon">
<icon-expand /> <icon-expand />
</div> </div>
<div>展开</div> <div>展开</div>
@@ -97,7 +97,7 @@
<!-- 收起 --> <!-- 收起 -->
<a-doption v-else <a-doption v-else
@click="() => item.expand = false"> @click="() => item.expand = false">
<div class="host-space-context-menu-icon"> <div class="terminal-context-menu-icon">
<icon-shrink /> <icon-shrink />
</div> </div>
<div>收起</div> <div>收起</div>

View File

@@ -185,7 +185,7 @@
import { dataColor } from '@/utils'; import { dataColor } from '@/utils';
import { tagColor } from '@/views/asset/host-list/types/const'; import { tagColor } from '@/views/asset/host-list/types/const';
import { updateHostAlias } from '@/api/asset/host-extra'; import { updateHostAlias } from '@/api/asset/host-extra';
import { openSshModalKey, PanelSessionType } from '../../types/terminal.const'; import { openSshSettingModalKey, PanelSessionType } from '../../types/terminal.const';
import { useTerminalStore } from '@/store'; import { useTerminalStore } from '@/store';
const props = defineProps<{ const props = defineProps<{
@@ -231,7 +231,7 @@
}; };
// 打开配置 // 打开配置
const openSetting = inject(openSshModalKey) as (record: HostQueryResponse) => void; const openSetting = inject(openSshSettingModalKey) as (record: HostQueryResponse) => void;
// 设置收藏 // 设置收藏
const setFavorite = async (item: HostQueryResponse) => { const setFavorite = async (item: HostQueryResponse) => {

View File

@@ -34,7 +34,7 @@
<script lang="ts" setup> <script lang="ts" setup>
import { onMounted, provide, ref, watch } from 'vue'; import { onMounted, provide, ref, watch } from 'vue';
import { NewConnectionType, openSshModalKey } from '../../types/terminal.const'; import { NewConnectionType, openSshSettingModalKey } from '../../types/terminal.const';
import { AuthorizedHostQueryResponse } from '@/api/asset/asset-authorized-data'; import { AuthorizedHostQueryResponse } from '@/api/asset/asset-authorized-data';
import { HostQueryResponse } from '@/api/asset/host'; import { HostQueryResponse } from '@/api/asset/host';
import HostGroupView from './host-group-view.vue'; import HostGroupView from './host-group-view.vue';
@@ -57,7 +57,7 @@
const sshModal = ref(); const sshModal = ref();
// 暴露打开 ssh 配置模态框 // 暴露打开 ssh 配置模态框
provide(openSshModalKey, (record: any) => { provide(openSshSettingModalKey, (record: any) => {
sshModal.value?.open(record); sshModal.value?.open(record);
}); });

View File

@@ -17,7 +17,7 @@
<!-- 启用-修改中 --> <!-- 启用-修改中 -->
<a-input v-if="item.editable && item.enabled" <a-input v-if="item.editable && item.enabled"
v-model="item.shortcutKey" v-model="item.shortcutKey"
:ref="setEditRef as unknown as VNodeRef" :ref="setAutoFocus as unknown as VNodeRef"
class="trigger-input" class="trigger-input"
size="small" size="small"
placeholder="请按下快捷键" placeholder="请按下快捷键"
@@ -67,7 +67,7 @@
<script lang="ts" setup> <script lang="ts" setup>
import type { TerminalShortcutKeyEditable } from '@/store/modules/terminal/types'; import type { TerminalShortcutKeyEditable } from '@/store/modules/terminal/types';
import type { VNodeRef } from 'vue'; import type { VNodeRef } from 'vue';
import { nextTick } from 'vue'; import { setAutoFocus } from '@/utils/dom';
defineProps<{ defineProps<{
title: string; title: string;
@@ -77,14 +77,6 @@
const emits = defineEmits(['setEditable', 'clearEditable', 'updateEnabled']); const emits = defineEmits(['setEditable', 'clearEditable', 'updateEnabled']);
// 设置 ref
const setEditRef = (el: HTMLElement) => {
// 自动聚焦
nextTick(() => {
el && el.focus();
});
};
// 修改启用状态 // 修改启用状态
const updateEnabledStatus = (item: TerminalShortcutKeyEditable, enabled: boolean) => { const updateEnabledStatus = (item: TerminalShortcutKeyEditable, enabled: boolean) => {
emits('updateEnabled', item, enabled); emits('updateEnabled', item, enabled);

View File

@@ -1,5 +1,30 @@
<template> <template>
<div></div> <a-modal v-model:visible="visible"
body-class="modal-form"
title-align="start"
:title="touch ? '创建文件' : '创建文件夹'"
:align-center="false"
:mask-closable="false"
:unmount-on-close="true"
:on-before-ok="handlerOk"
@close="handleClose">
<a-form :model="formModel"
ref="formRef"
label-align="right"
layout="horizontal"
:style="{ width: '460px' }"
:label-col-props="{ span: 6 }"
:wrapper-col-props="{ span: 18 }">
<!-- 文件路径 -->
<a-form-item field="path"
label="文件路径"
:rules="[{ required: true, message: '请输入文件路径' }]">
<a-input ref="pathRef"
v-model="formModel.path"
placeholder="请输入文件路径" />
</a-form-item>
</a-form>
</a-modal>
</template> </template>
<script lang="ts"> <script lang="ts">
@@ -9,6 +34,76 @@
</script> </script>
<script lang="ts" setup> <script lang="ts" setup>
import useVisible from '@/hooks/visible';
import useLoading from '@/hooks/loading';
import { nextTick, ref } from 'vue';
import { useTerminalStore } from '@/store';
import SftpSession from '../../handler/sftp-session';
const { visible, setVisible } = useVisible();
const { loading, setLoading } = useLoading();
const { sessionManager } = useTerminalStore();
const sessionId = ref();
const touch = ref(false);
const pathRef = ref();
const formRef = ref();
const formModel = ref({
path: ''
});
// 打开新增
const open = (session: string, path: string, isTouch: boolean) => {
sessionId.value = session;
formModel.value.path = path;
touch.value = isTouch;
setVisible(true);
// 自动聚焦
nextTick(() => {
pathRef.value?.focus();
});
};
defineExpose({ open });
// 确定
const handlerOk = async () => {
setLoading(true);
try {
// 验证参数
const error = await formRef.value.validate();
if (error) {
return false;
}
// 获取会话
const session = sessionManager.getSession(sessionId.value);
if (session instanceof SftpSession) {
if (touch.value) {
// 创建文件
session.touch(formModel.value.path);
} else {
// 创建文件夹
session.mkdir(formModel.value.path);
}
}
// 清空
handlerClear();
} catch (e) {
return false;
} finally {
setLoading(false);
}
};
// 关闭
const handleClose = () => {
handlerClear();
};
// 清空
const handlerClear = () => {
setLoading(false);
};
</script> </script>

View File

@@ -33,7 +33,7 @@
<!-- 根目录 --> <!-- 根目录 -->
<a-breadcrumb-item class="sftp-path-unit" <a-breadcrumb-item class="sftp-path-unit"
@click.stop="loadFileList('/')"> @click.stop="loadFileList('/')">
<icon-home /> <icon-storage />
</a-breadcrumb-item> </a-breadcrumb-item>
<!-- 子目录 --> <!-- 子目录 -->
<a-breadcrumb-item class="sftp-path-unit" <a-breadcrumb-item class="sftp-path-unit"
@@ -180,8 +180,9 @@
<script lang="ts" setup> <script lang="ts" setup>
import type { PathAnalysis } from '@/utils/file'; import type { PathAnalysis } from '@/utils/file';
import type { ISftpSession } from '../../types/terminal.type'; import type { ISftpSession } from '../../types/terminal.type';
import { nextTick, ref, watch } from 'vue'; import { inject, nextTick, ref, watch } from 'vue';
import { getParentPath, getPathAnalysis } from '@/utils/file'; import { getParentPath, getPathAnalysis } from '@/utils/file';
import { openSftpCreateModalKey } from '../../types/terminal.const';
const props = defineProps<{ const props = defineProps<{
currentPath: string; currentPath: string;
@@ -197,6 +198,8 @@
const pathInput = ref(''); const pathInput = ref('');
const pathInputRef = ref(); const pathInputRef = ref();
const openSftpCreateModal = inject(openSftpCreateModalKey) as (sessionId: string, path: string, isTouch: boolean) => void;
// 监听路径变化 // 监听路径变化
watch(() => props.currentPath, (path) => { watch(() => props.currentPath, (path) => {
if (path) { if (path) {
@@ -246,20 +249,19 @@
// 创建文件 // 创建文件
const createFile = () => { const createFile = () => {
// TODO openModal(true, "props.currentPath") openSftpCreateModal(props.session?.sessionId as string, props.currentPath + '/', true);
console.log(props.currentPath);
}; };
// 创建文件夹 // 创建文件夹
const createDir = () => { const createDir = () => {
// TODO openModal(false, "props.currentPath") openSftpCreateModal(props.session?.sessionId as string, props.currentPath + '/', false);
console.log(props.currentPath);
}; };
// 删除选中文件 // 删除选中文件
const deleteSelectFiles = () => { const deleteSelectFiles = () => {
// TODO confirm if (props.selectedFiles?.length) {
console.log(props.selectedFiles); props.session?.remove(props.selectedFiles);
}
}; };
// 上传文件 // 上传文件
@@ -279,8 +281,8 @@
<style lang="less" scoped> <style lang="less" scoped>
@action-num: 7; @action-num: 7;
@action-gap: 8px; @action-gap: 8px;
@action-width: 26px; @action-size: 26px;
@actions-width: @action-num * (@action-width + @action-gap); @actions-width: @action-num * (@action-size + @action-gap);
.sftp-table-header { .sftp-table-header {
width: 100%; width: 100%;
@@ -312,6 +314,7 @@
:deep(.sftp-path-unit) { :deep(.sftp-path-unit) {
cursor: pointer; cursor: pointer;
font-size: 12px;
&:hover { &:hover {
color: rgb(var(--arcoblue-6)); color: rgb(var(--arcoblue-6));
@@ -326,5 +329,7 @@
.header-action-icon { .header-action-icon {
font-size: 16px; font-size: 16px;
padding: 4px; padding: 4px;
width: @action-size;
height: @action-size;
} }
</style> </style>

View File

@@ -1,5 +1,6 @@
<template> <template>
<a-table row-key="path" <a-table row-key="path"
ref="tableRef"
class="sftp-table" class="sftp-table"
label-align="left" label-align="left"
:columns="columns" :columns="columns"
@@ -9,16 +10,19 @@
:data="list" :data="list"
:pagination="false" :pagination="false"
:bordered="false" :bordered="false"
@cell-mouse-enter="setEditable" :loading="loading"
@cell-mouse-leave="unsetEditable"> @cell-mouse-enter="setOperable"
@cell-mouse-leave="unsetOperable">
<!-- 文件搜索框 --> <!-- 文件搜索框 -->
<template #nameFilter="{ filterValue, setFilterValue, handleFilterConfirm, handleFilterReset}"> <template #nameFilter="{ filterValue, setFilterValue, handleFilterConfirm, handleFilterReset}">
<div class="name-filter"> <div class="name-filter">
<a-space direction="vertical"> <a-space direction="vertical">
<!-- 过滤输入框 --> <!-- 过滤输入框 -->
<a-input size="small" <a-input size="small"
:ref="setAutoFocus as unknown as VNodeRef"
:model-value="filterValue[0]" :model-value="filterValue[0]"
@input="(value) => setFilterValue([value])" /> @input="(value) => setFilterValue([value])"
@pressEnter="handleFilterConfirm" />
<!-- 按钮 --> <!-- 按钮 -->
<div class="name-filter-footer"> <div class="name-filter-footer">
<a-button size="small" @click="handleFilterConfirm">过滤</a-button> <a-button size="small" @click="handleFilterConfirm">过滤</a-button>
@@ -59,10 +63,24 @@
arrow-class="terminal-tooltip-content" arrow-class="terminal-tooltip-content"
content="复制路径"> content="复制路径">
<span class="click-icon-wrapper row-action-icon" <span class="click-icon-wrapper row-action-icon"
@click="copy(record.path, false)"> @click="copy(record.path, '已复制')">
<icon-copy /> <icon-copy />
</span> </span>
</a-tooltip> </a-tooltip>
<!-- 编辑内容 -->
<a-tooltip v-if="canEditable(record.attr)"
position="top"
:mini="true"
:overlay-inverse="true"
:auto-fix-position="false"
content-class="terminal-tooltip-content"
arrow-class="terminal-tooltip-content"
content="编辑内容">
<span class="click-icon-wrapper row-action-icon"
@click="editFile(record)">
<icon-edit />
</span>
</a-tooltip>
<!-- 删除 --> <!-- 删除 -->
<a-tooltip position="top" <a-tooltip position="top"
:mini="true" :mini="true"
@@ -125,12 +143,14 @@
<script lang="ts" setup> <script lang="ts" setup>
import type { TableData } from '@arco-design/web-vue/es/table/interface'; import type { TableData } from '@arco-design/web-vue/es/table/interface';
import type { SftpFile, ISftpSession } from '../../types/terminal.type'; import type { SftpFile, ISftpSession } from '../../types/terminal.type';
import { ref, computed } from 'vue'; import type { VNodeRef } from 'vue';
import { ref, computed, watch } from 'vue';
import { useRowSelection } from '@/types/table'; import { useRowSelection } from '@/types/table';
import { dateFormat } from '@/utils'; import { dateFormat } from '@/utils';
import columns from './types/table.columns';
import useCopy from '@/hooks/copy'; import useCopy from '@/hooks/copy';
import columns from './types/table.columns';
import { FILE_TYPE } from '../../types/terminal.const'; import { FILE_TYPE } from '../../types/terminal.const';
import { setAutoFocus } from '@/utils/dom';
const props = defineProps<{ const props = defineProps<{
session: ISftpSession | undefined; session: ISftpSession | undefined;
@@ -144,6 +164,11 @@
const rowSelection = useRowSelection({ width: 40 }); const rowSelection = useRowSelection({ width: 40 });
const { copy } = useCopy(); const { copy } = useCopy();
// 切换页面自动清空过滤
watch(() => props.list, () => {
tableRef.value?.clearFilters();
});
const selectedKeys = computed({ const selectedKeys = computed({
get() { get() {
return props.selectedFiles; return props.selectedFiles;
@@ -154,15 +179,16 @@
}); });
const editRowName = ref<string>(''); const editRowName = ref<string>('');
const tableRef = ref();
// 设置选中状态 // 设置可操作状态
const setEditable = (record: TableData) => { const setOperable = (record: TableData) => {
editRowName.value = record.name; editRowName.value = record.name;
record.hover = true; record.hover = true;
}; };
// 设置未选中状态 // 设置不可操作状态
const unsetEditable = (record: TableData) => { const unsetOperable = (record: TableData) => {
setTimeout(() => { setTimeout(() => {
// 等待后如果还是当前行 但是未被选中则代表已经被失焦 // 等待后如果还是当前行 但是未被选中则代表已经被失焦
if (record.name === editRowName.value && !record.hover) { if (record.name === editRowName.value && !record.hover) {
@@ -172,21 +198,30 @@
record.hover = false; record.hover = false;
}; };
// 是否可编辑
const canEditable = (attr: string) => {
const typeValue = formatFileType(attr).value;
// 非文件夹和链接文件可以编辑
return FILE_TYPE.DIRECTORY.value !== typeValue
&& FILE_TYPE.LINK_FILE.value !== typeValue;
};
// 点击文件名称 // 点击文件名称
const clickFilename = (record: TableData) => { const clickFilename = (record: TableData) => {
if (FILE_TYPE.DIRECTORY.value === formatFileType(record.attr).value) { if (FILE_TYPE.DIRECTORY.value === formatFileType(record.attr).value) {
// 进入文件夹 // 进入文件夹
emits('loadFile', record.path); emits('loadFile', record.path);
} else {
// 点击文件
// TODO VIEW
} }
}; };
// 编辑文件
const editFile = (record: TableData) => {
// TODO
};
// 删除文件 // 删除文件
const deleteFile = (path: string) => { const deleteFile = (path: string) => {
// TODO confirm props.session?.remove([path]);
console.log(path);
}; };
// 下载文件 // 下载文件

View File

@@ -7,7 +7,8 @@
<!-- 左侧面板表格 --> <!-- 左侧面板表格 -->
<template #first> <template #first>
<a-spin class="sftp-table-container" <a-spin class="sftp-table-container"
:loading="tableLoading"> :loading="tableLoading"
:hide-icon="true">
<!-- 表头 --> <!-- 表头 -->
<sftp-table-header class="sftp-table-header" <sftp-table-header class="sftp-table-header"
:current-path="currentPath" :current-path="currentPath"
@@ -27,6 +28,8 @@
<div>editor</div> <div>editor</div>
</template> </template>
</a-split> </a-split>
<!-- 创建文件模态框 -->
<sftp-create-modal ref="createModal" />
</div> </div>
</template> </template>
@@ -38,12 +41,14 @@
<script lang="ts" setup> <script lang="ts" setup>
import type { ISftpSession, SftpFile, TerminalTabItem } from '../../types/terminal.type'; import type { ISftpSession, SftpFile, TerminalTabItem } from '../../types/terminal.type';
import { onMounted, onUnmounted, ref } from 'vue'; import { onMounted, onUnmounted, provide, ref } from 'vue';
import { useTerminalStore } from '@/store'; import { useTerminalStore } from '@/store';
import { Message } from '@arco-design/web-vue'; import { Message } from '@arco-design/web-vue';
import useLoading from '@/hooks/loading'; import useLoading from '@/hooks/loading';
import { openSftpCreateModalKey } from '../../types/terminal.const';
import SftpTable from './sftp-table.vue'; import SftpTable from './sftp-table.vue';
import SftpTableHeader from './sftp-table-header.vue'; import SftpTableHeader from './sftp-table-header.vue';
import SftpCreateModal from './sftp-create-modal.vue';
const props = defineProps<{ const props = defineProps<{
tab: TerminalTabItem tab: TerminalTabItem
@@ -53,12 +58,18 @@
const { loading: tableLoading, setLoading: setTableLoading } = useLoading(true); const { loading: tableLoading, setLoading: setTableLoading } = useLoading(true);
const session = ref<ISftpSession>(); const session = ref<ISftpSession>();
const createModal = ref();
const currentPath = ref<string>(''); const currentPath = ref<string>('');
const fileList = ref<Array<SftpFile>>([]); const fileList = ref<Array<SftpFile>>([]);
const selectFiles = ref<Array<string>>([]); const selectFiles = ref<Array<string>>([]);
const splitSize = ref(1); const splitSize = ref(1);
const editView = ref(true); const editView = ref(true);
// 暴露打开创建方法
provide(openSftpCreateModalKey, (sessionId: string, path: string, isTouch: boolean) => {
createModal.value?.open(sessionId, path, isTouch);
});
// 连接成功回调 // 连接成功回调
const connectCallback = () => { const connectCallback = () => {
loadFiles(undefined); loadFiles(undefined);
@@ -70,16 +81,44 @@
session.value?.list(path); session.value?.list(path);
}; };
// 检查结果
const checkResult = (result: string, msg: string): boolean => {
const success = !!Number.parseInt(result);
if (!success) {
Message.error(msg);
}
return success;
};
// 接收列表回调 // 接收列表回调
const resolveList = (result: string, path: string, list: Array<SftpFile>) => { const resolveList = (result: string, path: string, list: Array<SftpFile>) => {
const success = !!Number.parseInt(result);
setTableLoading(false); setTableLoading(false);
if (!success) { if (!checkResult(result, '查询失败')) {
Message.error('查询失败');
return; return;
} }
currentPath.value = path; currentPath.value = path;
fileList.value = list; fileList.value = list;
selectFiles.value = [];
};
// 接收文件响应
const resolveFileAction = (result: string, msg: string) => {
setTableLoading(false);
// 检查结果
if (!checkResult(result, msg)) {
return;
}
Message.success('操作成功');
// 刷新列表
loadFiles(currentPath.value);
};
// 接收获取文件内容响应
const resolveSftpGetContent = (path: string, result: string, content: string) => {
};
// 接收修改文件内容响应
const resolveSftpSetContent = (result: string, msg: string) => {
}; };
// 初始化会话 // 初始化会话
@@ -87,7 +126,14 @@
// 创建终端处理器 // 创建终端处理器
session.value = await sessionManager.openSftp(props.tab, { session.value = await sessionManager.openSftp(props.tab, {
connectCallback, connectCallback,
resolveList resolveList,
resolveSftpMkdir: resolveFileAction,
resolveSftpTouch: resolveFileAction,
resolveSftpMove: resolveFileAction,
resolveSftpRemove: resolveFileAction,
resolveSftpChmod: resolveFileAction,
resolveSftpGetContent,
resolveSftpSetContent,
}); });
}); });

View File

@@ -34,7 +34,7 @@ const columns = [
sortable: { sortable: {
sortDirections: ['ascend', 'descend'], sortDirections: ['ascend', 'descend'],
}, },
width: 218, width: 234,
cellClass: 'action-cell', cellClass: 'action-cell',
}, },
] as TableColumnData[]; ] as TableColumnData[];

View File

@@ -1,5 +1,6 @@
import type { ISftpSession, ISftpSessionResolver, ITerminalChannel } from '../types/terminal.type'; import type { ISftpSession, ISftpSessionResolver, ITerminalChannel } from '../types/terminal.type';
import { InputProtocol } from '../types/terminal.protocol'; import { InputProtocol } from '../types/terminal.protocol';
import { Modal } from '@arco-design/web-vue';
// sftp 会话实现 // sftp 会话实现
export default class SftpSession implements ISftpSession { export default class SftpSession implements ISftpSession {
@@ -53,6 +54,71 @@ export default class SftpSession implements ISftpSession {
}); });
}; };
// 创建文件夹
mkdir(path: string) {
this.channel.send(InputProtocol.SFTP_MKDIR, {
sessionId: this.sessionId,
path
});
};
// 创建文件
touch(path: string) {
this.channel.send(InputProtocol.SFTP_TOUCH, {
sessionId: this.sessionId,
path
});
};
// 移动文件
move(path: string, target: string) {
this.channel.send(InputProtocol.SFTP_MOVE, {
sessionId: this.sessionId,
path,
target
});
};
// 删除文件
remove(path: string[]) {
Modal.confirm({
title: '删除确认',
content: `确定要删除 ${path} 吗? 确定后将立即删除且无法恢复!`,
onOk: () => {
this.channel.send(InputProtocol.SFTP_REMOVE, {
sessionId: this.sessionId,
path: path.join('|')
});
}
});
};
// 修改权限
chmod(path: string, mod: number) {
this.channel.send(InputProtocol.SFTP_CHMOD, {
sessionId: this.sessionId,
path,
mod
});
};
// 获取内容
getContent(path: string) {
this.channel.send(InputProtocol.SFTP_GET_CONTENT, {
sessionId: this.sessionId,
path
});
};
// 修改内容
setContent(path: string, content: string) {
this.channel.send(InputProtocol.SFTP_SET_CONTENT, {
sessionId: this.sessionId,
path,
content
});
};
// 断开连接 // 断开连接
disconnect(): void { disconnect(): void {
// 发送关闭消息 // 发送关闭消息

View File

@@ -119,4 +119,53 @@ export default class TerminalOutputProcessor implements ITerminalOutputProcessor
session && session.resolver.resolveList(result, path, JSON.parse(body)); session && session.resolver.resolveList(result, path, JSON.parse(body));
} }
// 处理 SFTP 创建文件夹
processSftpMkdir({ sessionId, result, msg }: OutputPayload): void {
// 获取会话
const session = this.sessionManager.getSession<ISftpSession>(sessionId);
session && session.resolver.resolveSftpMkdir(result, msg);
}
// 处理 SFTP 创建文件
processSftpTouch({ sessionId, result, msg }: OutputPayload): void {
// 获取会话
const session = this.sessionManager.getSession<ISftpSession>(sessionId);
session && session.resolver.resolveSftpTouch(result, msg);
}
// 处理 SFTP 移动文件
processSftpMove({ sessionId, result, msg }: OutputPayload): void {
// 获取会话
const session = this.sessionManager.getSession<ISftpSession>(sessionId);
session && session.resolver.resolveSftpMove(result, msg);
}
// 处理 SFTP 删除文件
processSftpRemove({ sessionId, result, msg }: OutputPayload): void {
// 获取会话
const session = this.sessionManager.getSession<ISftpSession>(sessionId);
session && session.resolver.resolveSftpRemove(result, msg);
}
// 处理 SFTP 修改文件权限
processSftpChmod({ sessionId, result, msg }: OutputPayload): void {
// 获取会话
const session = this.sessionManager.getSession<ISftpSession>(sessionId);
session && session.resolver.resolveSftpChmod(result, msg);
}
// 处理 SFTP 获取文件内容
processSftpGetContent({ sessionId, path, result, content }: OutputPayload): void {
// 获取会话
const session = this.sessionManager.getSession<ISftpSession>(sessionId);
session && session.resolver.resolveSftpGetContent(path, result, content);
}
// 处理 SFTP 修改文件内容
processSftpSetContent({ sessionId, result, msg }: OutputPayload) {
// 获取会话
const session = this.sessionManager.getSession<ISftpSession>(sessionId);
session && session.resolver.resolveSftpSetContent(result, msg);
}
} }

View File

@@ -35,6 +35,41 @@ export const InputProtocol = {
type: 'ls', type: 'ls',
template: ['type', 'sessionId', 'showHiddenFile', 'path'] template: ['type', 'sessionId', 'showHiddenFile', 'path']
}, },
// SFTP 创建文件夹
SFTP_MKDIR: {
type: 'mk',
template: ['type', 'sessionId', 'path']
},
// SFTP 创建文件
SFTP_TOUCH: {
type: 'to',
template: ['type', 'sessionId', 'path']
},
// SFTP 移动文件
SFTP_MOVE: {
type: 'mv',
template: ['type', 'sessionId', 'path', 'target']
},
// SFTP 删除文件
SFTP_REMOVE: {
type: 'rm',
template: ['type', 'sessionId', 'path']
},
// SFTP 修改文件权限
SFTP_CHMOD: {
type: 'cm',
template: ['type', 'sessionId', 'path', 'mod']
},
// SFTP 获取内容
SFTP_GET_CONTENT: {
type: 'gc',
template: ['type', 'sessionId', 'path']
},
// SFTP 修改内容
SFTP_SET_CONTENT: {
type: 'sc',
template: ['type', 'sessionId', 'path', 'content']
},
}; };
// 输出协议 // 输出协议
@@ -72,7 +107,49 @@ export const OutputProtocol = {
// SFTP 文件列表 // SFTP 文件列表
SFTP_LIST: { SFTP_LIST: {
type: 'ls', type: 'ls',
template: ['type', 'sessionId', 'result', 'path', 'body'], template: ['type', 'sessionId', 'path', 'result', 'body'],
processMethod: 'processSftpList' processMethod: 'processSftpList'
}, },
// SFTP 创建文件夹
SFTP_MKDIR: {
type: 'mk',
template: ['type', 'sessionId', 'result', 'msg'],
processMethod: 'processSftpMkdir'
},
// SFTP 创建文件
SFTP_TOUCH: {
type: 'to',
template: ['type', 'sessionId', 'result', 'msg'],
processMethod: 'processSftpTouch'
},
// SFTP 移动文件
SFTP_MOVE: {
type: 'mv',
template: ['type', 'sessionId', 'result', 'msg'],
processMethod: 'processSftpMove'
},
// SFTP 删除文件
SFTP_REMOVE: {
type: 'rm',
template: ['type', 'sessionId', 'result', 'msg'],
processMethod: 'processSftpRemove'
},
// SFTP 修改文件权限
SFTP_CHMOD: {
type: 'cm',
template: ['type', 'sessionId', 'result', 'msg'],
processMethod: 'processSftpChmod'
},
// SFTP 获取文件内容
SFTP_GET_CONTENT: {
type: 'gc',
template: ['type', 'sessionId', 'path', 'result', 'content'],
processMethod: 'processSftpGetContent'
},
// SFTP 修改文件内容
SFTP_SET_CONTENT: {
type: 'sc',
template: ['type', 'sessionId', 'result', 'msg'],
processMethod: 'processSftpSetContent'
},
}; };

View File

@@ -180,6 +180,20 @@ export interface ITerminalOutputProcessor {
processSshOutput: (payload: OutputPayload) => void; processSshOutput: (payload: OutputPayload) => void;
// 处理 SFTP 文件列表 // 处理 SFTP 文件列表
processSftpList: (payload: OutputPayload) => void; processSftpList: (payload: OutputPayload) => void;
// 处理 SFTP 创建文件夹
processSftpMkdir: (payload: OutputPayload) => void;
// 处理 SFTP 创建文件
processSftpTouch: (payload: OutputPayload) => void;
// 处理 SFTP 移动文件
processSftpMove: (payload: OutputPayload) => void;
// 处理 SFTP 删除文件
processSftpRemove: (payload: OutputPayload) => void;
// 处理 SFTP 修改文件权限
processSftpChmod: (payload: OutputPayload) => void;
// 处理 SFTP 获取文件内容
processSftpGetContent: (payload: OutputPayload) => void;
// 处理 SFTP 修改文件内容
processSftpSetContent: (payload: OutputPayload) => void;
} }
// xterm dom 元素引用 // xterm dom 元素引用
@@ -299,6 +313,20 @@ export interface ISftpSession extends ITerminalSession {
setShowHiddenFile: (show: boolean) => void; setShowHiddenFile: (show: boolean) => void;
// 查询文件列表 // 查询文件列表
list: (path: string | undefined) => void; list: (path: string | undefined) => void;
// 创建文件夹
mkdir: (path: string) => void;
// 创建文件
touch: (path: string) => void;
// 移动文件
move: (path: string, target: string) => void;
// 删除文件
remove: (path: string[]) => void;
// 修改权限
chmod: (path: string, mod: number) => void;
// 获取内容
getContent: (path: string) => void;
// 修改内容
setContent: (path: string, content: string) => void;
} }
// sftp 会话接收器定义 // sftp 会话接收器定义
@@ -307,6 +335,20 @@ export interface ISftpSessionResolver {
connectCallback: () => void; connectCallback: () => void;
// 接受文件列表响应 // 接受文件列表响应
resolveList: (result: string, path: string, list: Array<SftpFile>) => void; resolveList: (result: string, path: string, list: Array<SftpFile>) => void;
// 接收创建文件夹响应
resolveSftpMkdir: (result: string, msg: string) => void;
// 接收创建文件响应
resolveSftpTouch: (result: string, msg: string) => void;
// 接收移动文件响应
resolveSftpMove: (result: string, msg: string) => void;
// 接收删除文件响应
resolveSftpRemove: (result: string, msg: string) => void;
// 接收修改文件权限响应
resolveSftpChmod: (result: string, msg: string) => void;
// 接收获取文件内容响应
resolveSftpGetContent: (path: string, result: string, content: string) => void;
// 接收修改文件内容响应
resolveSftpSetContent: (result: string, msg: string) => void;
} }
// sftp 文件 // sftp 文件