From afda15de682d44fd2f795c393dd9d2af990b27c1 Mon Sep 17 00:00:00 2001 From: chaos-zhu Date: Tue, 13 Aug 2024 15:10:41 +0800 Subject: [PATCH] =?UTF-8?q?:sparkles:=20SFTP=E6=94=AF=E6=8C=81=E4=B8=8A?= =?UTF-8?q?=E4=BC=A0=E6=96=87=E4=BB=B6=E5=A4=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- server/app/socket/sftp.js | 78 +++++++++--------- server/package.json | 1 + web/src/views/terminal/components/sftp.vue | 93 ++++++++++++++++------ yarn.lock | 9 +++ 4 files changed, 117 insertions(+), 64 deletions(-) diff --git a/server/app/socket/sftp.js b/server/app/socket/sftp.js index 4c75c7e..78b1e0e 100644 --- a/server/app/socket/sftp.js +++ b/server/app/socket/sftp.js @@ -1,29 +1,14 @@ const { Server } = require('socket.io') const SFTPClient = require('ssh2-sftp-client') const rawPath = require('path') -const fs = require('fs') -const { readHostList, readSSHRecord, verifyAuthSync, RSADecryptSync, AESDecryptSync } = require('../utils') +const fs = require('fs-extra') + +const { readHostList, readSSHRecord, verifyAuthSync, AESDecryptSync } = require('../utils') const { sftpCacheDir } = require('../config') const CryptoJS = require('crypto-js') -function clearDir(path, rmSelf = false) { - let files = [] - if(!fs.existsSync(path)) return consola.info('clearDir: 目标文件夹不存在') - files = fs.readdirSync(path) - files.forEach((file) => { - let curPath = path + '/' + file - if(fs.statSync(curPath).isDirectory()){ - clearDir(curPath) //递归删除文件夹 - fs.rmdirSync(curPath) // 删除文件夹 - } else { - fs.unlinkSync(curPath) //删除文件 - } - }) - if(rmSelf) fs.rmdirSync(path) - consola.success('clearDir: 已清空缓存文件') -} +// 读取切片 const pipeStream = (path, writeStream) => { - // console.log('path', path) return new Promise(resolve => { const readStream = fs.createReadStream(path) readStream.on('end', () => { @@ -110,7 +95,7 @@ function listenInput(sftpClient, socket) { } }) socket.on('up_file', async ({ targetPath, fullPath, name, file }) => { - console.log({ targetPath, fullPath, name, file }) + // console.log({ targetPath, fullPath, name, file }) const exists = await sftpClient.exists(targetPath) if(!exists) return socket.emit('not_exists_dir', '文件夹不存在或当前不可访问') try { @@ -125,43 +110,54 @@ function listenInput(sftpClient, socket) { } }) + // 上传文件夹先在目标sftp服务器创建文件夹 + socket.on('create_remote_dir', async ({ targetDirPath, folderName }) => { + let fullPath = rawPath.posix.join(targetDirPath, folderName) + consola.info('创建远程服务器文件夹:', fullPath) + const exists = await sftpClient.exists(fullPath) + if(exists) return socket.emit('is_exists_dir', '上传文件夹失败,文件夹已存在') + let res = await sftpClient.mkdir(fullPath) + consola.success('创建远程服务器文件夹成功:', fullPath) + socket.emit('create_remote_dir_success', res) + }) + /** 分片上传 */ // 1. 创建本地缓存文件夹 let md5List = [] - socket.on('create_cache_dir', async ({ targetPath, name }) => { - // console.log({ targetPath, name }) - const exists = await sftpClient.exists(targetPath) + socket.on('create_cache_dir', async ({ targetDirPath, name }) => { + // console.log({ targetDirPath, name }) + const exists = await sftpClient.exists(targetDirPath) if(!exists) return socket.emit('not_exists_dir', '文件夹不存在或当前不可访问') md5List = [] const localPath = rawPath.join(sftpCacheDir, name) - if(fs.existsSync(localPath)) clearDir(localPath) // 清空目录 - fs.mkdirSync(localPath, { recursive: true }) - console.log('================create_cache_success================') + fs.emptyDirSync(localPath) // 不存在会创建,存在则清空 socket.emit('create_cache_success') }) + // 2. 上传分片 socket.on('up_file_slice', async ({ name, sliceFile, fileIndex }) => { // console.log('up_file_slice:', fileIndex, name) try { let md5 = `${ fileIndex }.${ CryptoJS.MD5(fileIndex+name).toString() }` - const localPath = rawPath.join(sftpCacheDir, name, md5) - md5List.push(localPath) - fs.writeFileSync(localPath, sliceFile) + const md5LocalPath = rawPath.join(sftpCacheDir, name, md5) + md5List.push(md5LocalPath) + fs.writeFileSync(md5LocalPath, sliceFile) socket.emit('up_file_slice_success', md5) } catch (error) { consola.error('up_file_slice Error', error.message) socket.emit('up_file_slice_fail', error.message) } }) - socket.on('up_file_slice_over', async ({ name, fullPath, range, size }) => { - const resultDirPath = rawPath.join(sftpCacheDir, name) + // 3. 完成上传 + socket.on('up_file_slice_over', async ({ name, targetFilePath, range, size }) => { + const md5CacheDirPath = rawPath.join(sftpCacheDir, name) const resultFilePath = rawPath.join(sftpCacheDir, name, name) + fs.ensureDirSync(md5CacheDirPath) try { console.log('md5List: ', md5List) const arr = md5List.map((chunkFilePath, index) => { return pipeStream( chunkFilePath, - // 指定位置创建可写流 - fs.createWriteStream(resultFilePath, { + fs.createWriteStream(resultFilePath, { // 指定位置创建可写流 start: index * range, end: (index + 1) * range }) @@ -170,7 +166,7 @@ function listenInput(sftpClient, socket) { md5List = [] await Promise.all(arr) let timer = null - let res = await sftpClient.fastPut(resultFilePath, fullPath, { + let res = await sftpClient.fastPut(resultFilePath, targetFilePath, { step: step => { if(timer) return timer = setTimeout(() => { @@ -183,11 +179,14 @@ function listenInput(sftpClient, socket) { }) consola.success('sftp上传成功: ', res) socket.emit('up_file_success', res) - clearDir(resultDirPath, true) // 传服务器后移除文件夹及其文件 } catch (error) { consola.error('sftp上传失败: ', error.message) socket.emit('up_file_fail', error.message) - clearDir(resultDirPath, true) // 传服务器后移除文件夹及其文件 + } finally { + fs.remove(md5CacheDirPath) + .then(() => { + console.log('clean md5CacheDirPath:', md5CacheDirPath) + }) } }) } @@ -237,6 +236,7 @@ module.exports = (httpServer) => { .connect(authInfo) .then(() => { consola.success('连接Sftp成功:', host) + fs.ensureDirSync(sftpCacheDir) return sftpClient.list('/') }) .then((rootLs) => { @@ -260,8 +260,10 @@ module.exports = (httpServer) => { }) .finally(() => { sftpClient = null - const cacheDir = rawPath.join(sftpCacheDir) - clearDir(cacheDir) + fs.emptyDir(sftpCacheDir) + .then(() => { + consola.success('clean sftpCacheDir: ', sftpCacheDir) + }) }) }) }) diff --git a/server/package.json b/server/package.json index ee5de49..8ca7b3a 100644 --- a/server/package.json +++ b/server/package.json @@ -26,6 +26,7 @@ "cross-env": "^7.0.3", "crypto-js": "^4.2.0", "dotenv": "^16.4.5", + "fs-extra": "^11.2.0", "global": "^4.4.0", "jsonwebtoken": "^9.0.2", "koa": "^2.15.3", diff --git a/web/src/views/terminal/components/sftp.vue b/web/src/views/terminal/components/sftp.vue index 7135463..7b1b312 100644 --- a/web/src/views/terminal/components/sftp.vue +++ b/web/src/views/terminal/components/sftp.vue @@ -57,30 +57,40 @@ > - + +
+
-
- + +
$store.token) const curPath = computed(() => paths.value.join('/').replace(/\/{2,}/g, '/')) @@ -325,6 +336,7 @@ const listenSftp = () => { } const openRootChild = (item) => { + if (showFileProgress.value) return $message.warning('需等待当前任务完成') const { name, type } = item if (isDir(type)) { childDirLoading.value = true @@ -341,6 +353,7 @@ const openRootChild = (item) => { } const openTarget = (item) => { + if (showFileProgress.value) return $message.warning('需等待当前任务完成') const { name, type, size } = item if (isDir(type)) { paths.value.push(name) @@ -375,6 +388,7 @@ const selectFile = (item) => { } const handleReturn = () => { + if (showFileProgress.value) return $message.warning('需等待当前任务完成') if (paths.value.length === 1) return paths.value.pop() openDir() @@ -385,6 +399,7 @@ const handleRefresh = () => { } const handleDownload = () => { + if (showFileProgress.value) return $message.warning('需等待当前任务完成') if (curTarget.value === null) return $message.warning('先选择一个文件') const { name, size, type } = curTarget.value if (isDir(type)) return $message.error('暂不支持下载文件夹') @@ -407,6 +422,7 @@ const handleDownload = () => { } const handleDelete = () => { + if (showFileProgress.value) return $message.warning('需等待当前任务完成') if (curTarget.value === null) return $message.warning('先选择一个文件(夹)') const { name, type } = curTarget.value $messageBox.confirm(`确认删除:${ name }`, 'Warning', { @@ -424,38 +440,62 @@ const handleDelete = () => { }) } -const handleUpload = async (event) => { +const handleUploadFiles = async (event) => { if (showFileProgress.value) return $message.warning('需等待当前任务完成') let { files } = event.target + for (let file of files) { try { - await uploadFile(file) + const targetFilePath = getPath(file.name) + await uploadFile(file, targetFilePath) } catch (error) { - $message.error(error) + $message.error(`${ file.name }上传失败: ${ error }`) } } uploadFileRef.value = null } -const uploadFile = (file) => { +const handleUploadDir = async (event) => { + if (showFileProgress.value) return $message.warning('需等待当前任务完成') + let { files } = event.target + if(files.length === 0) return $message.warning('不允许上传空文件夹') + let folderName = files[0].webkitRelativePath.split('/')[0] + console.log(folderName) + console.log(files) + let targetDirPath = curPath.value + socket.value.emit('create_remote_dir', { targetDirPath, folderName }) + socket.value.once('is_exists_dir', (res) => { $message.error(res) }) + socket.value.once('create_remote_dir_success', async () => { + for (let file of files) { + let fullFilePath = getPath(`${ folderName }/${ file.name }`) + try { + await uploadFile(file, fullFilePath) + } catch (error) { + $message.error(`${ file.name }上传失败: ${ error }`) + } + } + uploadDirRef.value = null + }) +} + +const uploadFile = (file, targetFilePath) => { return new Promise((resolve, reject) => { if (!file) return reject('file is not defined') - if ((file.size / 1024 / 1024) > 1000) { - $message.warn('用网页传这么大文件你是认真的吗?') - } - let reader = new - FileReader() + // if ((file.size / 1024 / 1024) > 1000) { + // $message.warn('用网页传这么大文件你是认真的吗?') + // } + let reader = new FileReader() reader.onload = async () => { const { name } = file - const fullPath = getPath(name) - const targetPath = curPath.value + const targetDirPath = curPath.value curUploadFileName.value = name - socket.value.emit('create_cache_dir', { targetPath, name }) + const size = file.size + if(size === 0) return reject('文件大小为0KB, 无法上传') + socket.value.emit('create_cache_dir', { targetDirPath, name }) socket.value.once('create_cache_success', async () => { let start = 0 let end = 0 const range = 1024 * 512 // 每段512KB - const size = file.size let fileIndex = 0 let multipleFlag = false try { @@ -471,7 +511,7 @@ const uploadFile = (file) => { await uploadSliceFile({ name, sliceFile, fileIndex }) upFileProgress.value = parseInt((fileIndex / totalSliceCount * 100) / 2) } - socket.value.emit('up_file_slice_over', { name, fullPath, range, size }) + socket.value.emit('up_file_slice_over', { name, targetFilePath, range, size }) socket.value.once('up_file_success', () => { if (multipleFlag) return handleRefresh() @@ -523,6 +563,7 @@ const uploadSliceFile = (fileInfo) => { } const openDir = (path = '', tips = true) => { + if (showFileProgress.value) return $message.warning('需等待当前任务完成') childDirLoading.value = true curTarget.value = null socket.value.emit('open_dir', path || curPath.value, tips) diff --git a/yarn.lock b/yarn.lock index e4d43b9..07308af 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2524,6 +2524,15 @@ fs-extra@^10.0.0: jsonfile "^6.0.1" universalify "^2.0.0" +fs-extra@^11.2.0: + version "11.2.0" + resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-11.2.0.tgz#e70e17dfad64232287d01929399e0ea7c86b0e5b" + integrity sha512-PmDi3uwK5nFuXh7XDTlVnS17xJS7vW36is2+w3xcv8SVxiB4NyATf4ctkVY5bkSjX0Y4nbvZCq1/EjtEyr9ktw== + dependencies: + graceful-fs "^4.2.0" + jsonfile "^6.0.1" + universalify "^2.0.0" + fs-extra@^8.1.0: version "8.1.0" resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-8.1.0.tgz#49d43c45a88cd9677668cb7be1b46efdb8d2e1c0"