✨ SFTP支持上传文件夹
This commit is contained in:
parent
b8da64f8dd
commit
afda15de68
@ -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)
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
@ -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",
|
||||
|
@ -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)
|
||||
|
@ -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"
|
||||
|
Loading…
x
Reference in New Issue
Block a user