🔨 sftp 文件列表.

This commit is contained in:
lijiahang
2024-02-10 00:05:27 +08:00
parent 8f8858fda0
commit c723eb4ac3
7 changed files with 243 additions and 68 deletions

View File

@@ -26,7 +26,7 @@ export function readFileText(e: File, encoding = 'UTF-8'): Promise<string> {
/** /**
* 解析路径类型 * 解析路径类型
*/ */
type PathAnalysis = { export interface PathAnalysis {
name: string; name: string;
path: string; path: string;
} }
@@ -34,23 +34,19 @@ type PathAnalysis = {
/** /**
* 获取解析路径 * 获取解析路径
*/ */
export function getPathAnalysis(analysisPath: string, paths: PathAnalysis[] = []): PathAnalysis[] { export function getPathAnalysis(path: string, paths: PathAnalysis[] = []): PathAnalysis[] {
const lastSymbol = analysisPath.lastIndexOf('/'); const lastSeparatorIndex = path.lastIndexOf('/');
if (lastSymbol === -1) { if (lastSeparatorIndex === -1) {
paths.unshift({
name: '/',
path: '/'
});
return paths; return paths;
} }
const name = analysisPath.substring(lastSymbol, analysisPath.length); const name = path.substring(lastSeparatorIndex, path.length);
if (!isEmptyStr(name) && name !== '/') { if (!isEmptyStr(name) && name !== '/') {
paths.unshift({ paths.unshift({
name: name.substring(1, name.length), name: name.substring(1, name.length),
path: analysisPath path: path
}); });
} }
return getPathAnalysis(analysisPath.substring(0, lastSymbol), paths); return getPathAnalysis(path.substring(0, lastSeparatorIndex), paths);
} }
/** /**
@@ -58,7 +54,7 @@ export function getPathAnalysis(analysisPath: string, paths: PathAnalysis[] = []
*/ */
export function getPath(path: string) { export function getPath(path: string) {
return path.replace(new RegExp('\\\\+', 'g'), '/') return path.replace(new RegExp('\\\\+', 'g'), '/')
.replace(new RegExp('/+', 'g'), '/'); .replace(new RegExp('/+', 'g'), '/');
} }
/** /**

View File

@@ -11,27 +11,68 @@
content-class="terminal-tooltip-content" content-class="terminal-tooltip-content"
arrow-class="terminal-tooltip-content" arrow-class="terminal-tooltip-content"
content="返回上级"> content="返回上级">
<span class="click-icon-wrapper header-action-icon mr4"> <span class="click-icon-wrapper header-action-icon mr4"
@click="backParentPath">
<icon-left /> <icon-left />
</span> </span>
</a-tooltip> </a-tooltip>
<!-- 当前路径 --> <!-- 当前路径 -->
<div class="sftp-path-wrapper"> <div class="sftp-path-wrapper"
<a-breadcrumb> @click="setPathEditable(true)">
<!-- 分隔符 --> <!-- 路径输入框 -->
<template #separator> <div v-if="pathEditable">
<icon-right /> <a-input v-model="pathInput"
</template> placeholder="文件夹路径"
<a-breadcrumb-item class="sftp-path-unit">/</a-breadcrumb-item> allow-clear
<a-breadcrumb-item class="sftp-path-unit">root</a-breadcrumb-item> @press-enter="doChangePath" />
<a-breadcrumb-item class="sftp-path-unit">orion</a-breadcrumb-item> </div>
<a-breadcrumb-item class="sftp-path-unit">space</a-breadcrumb-item> <!-- 路径视图 -->
<a-breadcrumb-item class="sftp-path-unit">logs</a-breadcrumb-item> <a-breadcrumb v-else>
<!-- 根目录 -->
<a-breadcrumb-item class="sftp-path-unit"
@click.stop="loadFileList('/')">
<icon-home />
</a-breadcrumb-item>
<!-- 子目录 -->
<a-breadcrumb-item class="sftp-path-unit"
v-for="path in analysisPaths"
@click.stop="loadFileList(path.path)">
{{ path.name }}
</a-breadcrumb-item>
</a-breadcrumb> </a-breadcrumb>
</div> </div>
</div> </div>
<!-- 右侧操作 --> <!-- 路径编辑模式-右侧操作 -->
<a-space class="sftp-table-header-right"> <a-space v-if="pathEditable" class="sftp-table-header-right">
<!-- 进入 -->
<a-tooltip 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 header-action-icon"
@click="doChangePath">
<icon-right />
</span>
</a-tooltip>
<!-- 取消 -->
<a-tooltip 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 header-action-icon"
@click="setPathEditable(false)">
<icon-stop />
</span>
</a-tooltip>
</a-space>
<!-- 非路径编辑模式-右侧操作 -->
<a-space v-else class="sftp-table-header-right">
<!-- 刷新 --> <!-- 刷新 -->
<a-tooltip position="top" <a-tooltip position="top"
:mini="true" :mini="true"
@@ -40,7 +81,8 @@
content-class="terminal-tooltip-content" content-class="terminal-tooltip-content"
arrow-class="terminal-tooltip-content" arrow-class="terminal-tooltip-content"
content="刷新"> content="刷新">
<span class="click-icon-wrapper header-action-icon"> <span class="click-icon-wrapper header-action-icon"
@click="loadFileList">
<icon-refresh /> <icon-refresh />
</span> </span>
</a-tooltip> </a-tooltip>
@@ -51,9 +93,11 @@
:auto-fix-position="false" :auto-fix-position="false"
content-class="terminal-tooltip-content" content-class="terminal-tooltip-content"
arrow-class="terminal-tooltip-content" arrow-class="terminal-tooltip-content"
content="显示隐藏文件"> :content="showHiddenFile ? '不显示隐藏文件' : '显示隐藏文件'">
<span class="click-icon-wrapper header-action-icon"> <span class="click-icon-wrapper header-action-icon"
<icon-eye /> @click="toggleShowHiddenFile">
<icon-eye-invisible v-if="showHiddenFile" />
<icon-eye v-else />
</span> </span>
</a-tooltip> </a-tooltip>
<!-- 创建文件 --> <!-- 创建文件 -->
@@ -64,7 +108,8 @@
content-class="terminal-tooltip-content" content-class="terminal-tooltip-content"
arrow-class="terminal-tooltip-content" arrow-class="terminal-tooltip-content"
content="创建文件"> content="创建文件">
<span class="click-icon-wrapper header-action-icon"> <span class="click-icon-wrapper header-action-icon"
@click="createFile">
<icon-drive-file /> <icon-drive-file />
</span> </span>
</a-tooltip> </a-tooltip>
@@ -76,23 +121,25 @@
content-class="terminal-tooltip-content" content-class="terminal-tooltip-content"
arrow-class="terminal-tooltip-content" arrow-class="terminal-tooltip-content"
content="创建文件夹"> content="创建文件夹">
<span class="click-icon-wrapper header-action-icon"> <span class="click-icon-wrapper header-action-icon"
@click="createDir">
<icon-folder-add /> <icon-folder-add />
</span> </span>
</a-tooltip> </a-tooltip>
<!-- 删除 --> <!-- 删除选中文件 -->
<a-tooltip position="top" <a-tooltip position="top"
:mini="true" :mini="true"
:overlay-inverse="true" :overlay-inverse="true"
:auto-fix-position="false" :auto-fix-position="false"
content-class="terminal-tooltip-content" content-class="terminal-tooltip-content"
arrow-class="terminal-tooltip-content" arrow-class="terminal-tooltip-content"
content="删除"> content="删除选中文件">
<span class="click-icon-wrapper header-action-icon"> <span class="click-icon-wrapper header-action-icon"
@click="deleteSelectFiles">
<icon-delete /> <icon-delete />
</span> </span>
</a-tooltip> </a-tooltip>
<!-- 复制 --> <!-- 复制 FIXME 不行就删除 -->
<a-tooltip position="top" <a-tooltip position="top"
:mini="true" :mini="true"
:overlay-inverse="true" :overlay-inverse="true"
@@ -100,11 +147,11 @@
content-class="terminal-tooltip-content" content-class="terminal-tooltip-content"
arrow-class="terminal-tooltip-content" arrow-class="terminal-tooltip-content"
content="复制"> content="复制">
<span class="click-icon-wrapper header-action-icon"> <span v-if="false" class="click-icon-wrapper header-action-icon">
<icon-copy /> <icon-copy />
</span> </span>
</a-tooltip> </a-tooltip>
<!-- 移动 --> <!-- 移动 FIXME 不行就删除 -->
<a-tooltip position="top" <a-tooltip position="top"
:mini="true" :mini="true"
:overlay-inverse="true" :overlay-inverse="true"
@@ -124,7 +171,8 @@
content-class="terminal-tooltip-content" content-class="terminal-tooltip-content"
arrow-class="terminal-tooltip-content" arrow-class="terminal-tooltip-content"
content="上传"> content="上传">
<span class="click-icon-wrapper header-action-icon"> <span class="click-icon-wrapper header-action-icon"
@click="uploadFile">
<icon-upload /> <icon-upload />
</span> </span>
</a-tooltip> </a-tooltip>
@@ -136,7 +184,8 @@
content-class="terminal-tooltip-content" content-class="terminal-tooltip-content"
arrow-class="terminal-tooltip-content" arrow-class="terminal-tooltip-content"
content="下载"> content="下载">
<span class="click-icon-wrapper header-action-icon"> <span class="click-icon-wrapper header-action-icon"
@click="downloadFile">
<icon-download /> <icon-download />
</span> </span>
</a-tooltip> </a-tooltip>
@@ -151,6 +200,91 @@
</script> </script>
<script lang="ts" setup> <script lang="ts" setup>
import type { PathAnalysis } from '@/utils/file';
import type { ISftpSession } from '../../types/terminal.type';
import { ref, watch } from 'vue';
import { getParentPath, getPathAnalysis } from '@/utils/file';
const props = defineProps<{
currentPath: string;
session: ISftpSession | undefined,
selectedFiles: Array<string>
}>();
const emits = defineEmits(['loadFile']);
const showHiddenFile = ref(false);
const analysisPaths = ref<Array<PathAnalysis>>([]);
const pathEditable = ref(false);
const pathInput = ref('');
// 监听路径变化
watch(() => props.currentPath, (path) => {
if (path) {
analysisPaths.value = getPathAnalysis(path);
} else {
analysisPaths.value = [];
}
});
// 返回上级目录
const backParentPath = () => {
loadFileList(getParentPath(props.currentPath));
};
// 设置命令编辑模式
const setPathEditable = (editable: boolean) => {
pathEditable.value = editable;
pathInput.value = editable ? props.currentPath : '';
};
// 执行修改目录
const doChangePath = () => {
loadFileList(pathInput.value);
setPathEditable(false);
};
// 加载文件列表
const loadFileList = (path: string = props.currentPath) => {
emits('loadFile', path);
};
// 设置是否显示隐藏文件
const toggleShowHiddenFile = () => {
showHiddenFile.value = !showHiddenFile.value;
// 设置显示状态并且刷新
if (props.session) {
props.session.setShowHiddenFile(showHiddenFile.value);
loadFileList();
}
};
// 创建文件
const createFile = () => {
// openModal(true, "props.currentPath")
};
// 创建文件夹
const createDir = () => {
// openModal(false, "props.currentPath")
};
// 删除选中文件
const deleteSelectFiles = () => {
// confirm
};
// 上传文件
const uploadFile = () => {
// openModal("props.currentPath")
};
// 下载文件
const downloadFile = () => {
};
// FIXME 图标宽度提成变量
</script> </script>

View File

@@ -1,5 +1,5 @@
<template> <template>
<a-table row-key="name" <a-table row-key="path"
class="sftp-table" class="sftp-table"
label-align="left" label-align="left"
:columns="columns" :columns="columns"
@@ -44,7 +44,7 @@
<!-- 修改时间/操作 --> <!-- 修改时间/操作 -->
<template #modifyTime="{ record }"> <template #modifyTime="{ record }">
<!-- 修改时间 --> <!-- 修改时间 -->
<span v-if="editName !== record.name">{{ dateFormat(new Date(record.modifyTime)) }}</span> <span v-if="editRowName !== record.name">{{ dateFormat(new Date(record.modifyTime)) }}</span>
<!-- 操作 --> <!-- 操作 -->
<a-space v-else> <a-space v-else>
<!-- 复制路径 --> <!-- 复制路径 -->
@@ -67,10 +67,10 @@
content-class="terminal-tooltip-content" content-class="terminal-tooltip-content"
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="deleteFile(record.path)"> @click="deleteFile(record.path)">
<icon-delete /> <icon-delete />
</span> </span>
</a-tooltip> </a-tooltip>
<!-- 下载 --> <!-- 下载 -->
<a-tooltip position="top" <a-tooltip position="top"
@@ -79,10 +79,10 @@
content-class="terminal-tooltip-content" content-class="terminal-tooltip-content"
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="downloadFile(record.path)"> @click="downloadFile(record.path)">
<icon-download /> <icon-download />
</span> </span>
</a-tooltip> </a-tooltip>
<!-- 移动 --> <!-- 移动 -->
<a-tooltip position="top" <a-tooltip position="top"
@@ -91,10 +91,10 @@
content-class="terminal-tooltip-content" content-class="terminal-tooltip-content"
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="moveFile(record.path)"> @click="moveFile(record.path)">
<icon-paste /> <icon-paste />
</span> </span>
</a-tooltip> </a-tooltip>
<!-- 提权 --> <!-- 提权 -->
<a-tooltip position="top" <a-tooltip position="top"
@@ -103,10 +103,10 @@
content-class="terminal-tooltip-content" content-class="terminal-tooltip-content"
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="chmodFile(record.path)"> @click="chmodFile(record.path, record.attr)">
<icon-user-group /> <icon-user-group />
</span> </span>
</a-tooltip> </a-tooltip>
</a-space> </a-space>
</template> </template>
@@ -129,20 +129,37 @@
import useCopy from '@/hooks/copy'; import useCopy from '@/hooks/copy';
import { FILE_TYPE } from '../../types/terminal.const'; import { FILE_TYPE } from '../../types/terminal.const';
import type { ISftpSession } from '../../types/terminal.type';
import useLoading from '@/hooks/loading';
import { useCacheStore } from '@/store';
import { computed } from 'vue/dist/vue';
const props = defineProps<{ const props = defineProps<{
session: ISftpSession | undefined;
list: Array<SftpFile>; list: Array<SftpFile>;
loading: boolean; loading: boolean;
selectedFiles: Array<string>;
}>(); }>();
const emits = defineEmits(['update:selectedFiles']);
const rowSelection = useRowSelection({ width: 40 }); const rowSelection = useRowSelection({ width: 40 });
const { copy } = useCopy(); const { copy } = useCopy();
const selectedKeys = ref<Array<string>>([]); const selectedKeys = computed({
const editName = ref<string>(''); get() {
return props.selectedFiles;
},
set(e) {
emits('update:selectedFiles', e);
}
});
const editRowName = ref<string>('');
// 设置选中状态 // 设置选中状态
const setEditable = (record: TableData) => { const setEditable = (record: TableData) => {
editName.value = record.name; editRowName.value = record.name;
record.hover = true; record.hover = true;
}; };
@@ -150,8 +167,8 @@
const unsetEditable = (record: TableData) => { const unsetEditable = (record: TableData) => {
setTimeout(() => { setTimeout(() => {
// 等待后如果还是当前行 但是未被选中则代表已经被失焦 // 等待后如果还是当前行 但是未被选中则代表已经被失焦
if (record.name === editName.value && !record.hover) { if (record.name === editRowName.value && !record.hover) {
editName.value = ''; editRowName.value = '';
} }
}, 20); }, 20);
record.hover = false; record.hover = false;
@@ -159,6 +176,7 @@
// 删除文件 // 删除文件
const deleteFile = (path: string) => { const deleteFile = (path: string) => {
// confirm
}; };
// 下载文件 // 下载文件
@@ -167,10 +185,13 @@
// 移动文件 // 移动文件
const moveFile = (path: string) => { const moveFile = (path: string) => {
// openModal('path')
}; };
// 文件提权 // 文件提权
const chmodFile = (path: string) => { const chmodFile = (path: string, attr: string) => {
// openModal('path','mod')
}; };
// 格式化文件类型 // 格式化文件类型

View File

@@ -6,11 +6,18 @@
:disabled="!editView"> :disabled="!editView">
<!-- 左侧面板表格 --> <!-- 左侧面板表格 -->
<template #first> <template #first>
<!-- FIXME spin -->
<div class="sftp-table-container"> <div class="sftp-table-container">
<!-- 表头 --> <!-- 表头 -->
<sftp-table-header class="sftp-table-header" /> <sftp-table-header class="sftp-table-header"
:current-path="currentPath"
:session="session"
:selected-files="selectFiles"
@load-file="loadFiles" />
<!-- 表格 --> <!-- 表格 -->
<sftp-table class="sftp-table-wrapper" <sftp-table class="sftp-table-wrapper"
v-model:selected-files="selectFiles"
:session="session"
:list="fileList" :list="fileList"
:loading="tableLoading" /> :loading="tableLoading" />
</div> </div>
@@ -48,19 +55,25 @@
const session = ref<ISftpSession>(); const session = ref<ISftpSession>();
const currentPath = ref<string>(''); const currentPath = ref<string>('');
const fileList = ref<Array<SftpFile>>(mockData); const fileList = ref<Array<SftpFile>>(mockData);
const selectFiles = ref<Array<string>>([]);
const splitSize = ref(1); const splitSize = ref(1);
const editView = ref(true); const editView = ref(true);
// 连接成功回调 // 连接成功回调
const connectCallback = () => { const connectCallback = () => {
loadFiles(undefined);
};
// 加载文件列表
const loadFiles = (path: string | undefined) => {
setTableLoading(true); setTableLoading(true);
session.value?.list(undefined); session.value?.list(path);
}; };
// 接收列表回调 // 接收列表回调
const resolveList = (result: string, path: string, list: Array<SftpFile>) => { const resolveList = (result: string, path: string, list: Array<SftpFile>) => {
const success = !!Number.parseInt(result); const success = !!Number.parseInt(result);
setLoading(false); setTableLoading(false);
if (!success) { if (!success) {
Message.error('查询失败'); Message.error('查询失败');
return; return;

View File

@@ -12,6 +12,8 @@ export default class SftpSession implements ISftpSession {
public resolver: ISftpSessionResolver; public resolver: ISftpSessionResolver;
private showHiddenFile: boolean;
private readonly channel: ITerminalChannel; private readonly channel: ITerminalChannel;
constructor(hostId: number, constructor(hostId: number,
@@ -21,6 +23,7 @@ export default class SftpSession implements ISftpSession {
this.sessionId = sessionId; this.sessionId = sessionId;
this.channel = channel; this.channel = channel;
this.connected = false; this.connected = false;
this.showHiddenFile = false;
this.resolver = undefined as unknown as ISftpSessionResolver; this.resolver = undefined as unknown as ISftpSessionResolver;
} }
@@ -36,10 +39,16 @@ export default class SftpSession implements ISftpSession {
this.resolver.connectCallback(); this.resolver.connectCallback();
} }
// 设置显示隐藏文件
setShowHiddenFile(show: boolean): void {
this.showHiddenFile = show;
}
// 查询文件列表 // 查询文件列表
list(path: string | undefined) { list(path: string | undefined) {
this.channel.send(InputProtocol.SFTP_LIST, { this.channel.send(InputProtocol.SFTP_LIST, {
sessionId: this.sessionId, sessionId: this.sessionId,
showHiddenFile: ~~this.showHiddenFile,
path path
}); });
}; };

View File

@@ -33,7 +33,7 @@ export const InputProtocol = {
// SFTP 文件列表 // SFTP 文件列表
SFTP_LIST: { SFTP_LIST: {
type: 'ls', type: 'ls',
template: ['type', 'sessionId', 'path'] template: ['type', 'sessionId', 'showHiddenFile', 'path']
}, },
}; };

View File

@@ -295,6 +295,8 @@ export interface ISftpSession extends ITerminalSession {
// 初始化 // 初始化
init: (resolver: ISftpSessionResolver) => void; init: (resolver: ISftpSessionResolver) => void;
// 设置显示隐藏文件
setShowHiddenFile: (show: boolean) => void;
// 查询文件列表 // 查询文件列表
list: (path: string | undefined) => void; list: (path: string | undefined) => void;
} }