SFTP支持上传文件夹

This commit is contained in:
chaos-zhu 2024-08-13 15:10:41 +08:00
parent b8da64f8dd
commit afda15de68
4 changed files with 117 additions and 64 deletions

View File

@ -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)
})
})
})
})

View File

@ -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",

View File

@ -57,30 +57,40 @@
>
</div>
</tooltip>
<tooltip content="上传到当前目录">
<el-dropdown trigger="click">
<div class="img">
<img
src="@/assets/image/system/upload.png"
style=" width: 19px; height: 19px; "
@click="uploadFileRef.click()"
>
<input
ref="uploadFileRef"
type="file"
style="display: none;"
multiple
@change="handleUpload"
@change="handleUploadFiles"
>
<input
ref="uploadDirRef"
style="display: none;"
type="file"
webkitdirectory
directory
@change="handleUploadDir"
>
</div>
</tooltip>
<!-- <tooltip content="搜索">
<div class="img">
<img
src="@/assets/image/system/search.png"
style="width: 20px; height: 20px; margin-top: 1px;"
>
</div>
</tooltip> -->
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item @click="uploadFileRef.click()">
上传文件
</el-dropdown-item>
<el-dropdown-item @click="uploadDirRef.click()">
上传文件夹
</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
</div>
<div class="filter-input">
<el-input
@ -181,6 +191,7 @@ const adjustRef = ref(null)
const sftpTabContainerRef = ref(null)
const childDirRef = ref(null)
const uploadFileRef = ref(null)
const uploadDirRef = ref(null)
const token = computed(() => $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)

View File

@ -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"