chaos-zhu bcfbe0d6b3
2022-12-12 20:35:52 +08:00

257 lines
9.7 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

const { Server } = require('socket.io')
const SFTPClient = require('ssh2-sftp-client')
const rawPath = require('path')
const fs = require('fs')
const { readSSHRecord, verifyAuth, RSADecrypt, AESDecrypt } = 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', () => {
fs.unlinkSync(path) // 删除已写入切片
resolve()
})
readStream.pipe(writeStream)
})
}
function listenInput(sftpClient, socket) {
socket.on('open_dir', async (path) => {
const exists = await sftpClient.exists(path)
if(!exists) return socket.emit('not_exists_dir', '目录不存在或当前不可访问')
try {
let dirLs = await sftpClient.list(path)
socket.emit('dir_ls', dirLs)
} catch (error) {
consola.error('open_dir Error', error.message)
socket.emit('sftp_error', error.message)
}
})
socket.on('rm_dir', async (path) => {
const exists = await sftpClient.exists(path)
if(!exists) return socket.emit('not_exists_dir', '目录不存在或当前不可访问')
try {
let res = await sftpClient.rmdir(path, true) // 递归删除
socket.emit('rm_success', res)
} catch (error) {
consola.error('rm_dir Error', error.message)
socket.emit('sftp_error', error.message)
}
})
socket.on('rm_file', async (path) => {
const exists = await sftpClient.exists(path)
if(!exists) return socket.emit('not_exists_dir', '文件不存在或当前不可访问')
try {
let res = await sftpClient.delete(path)
socket.emit('rm_success', res)
} catch (error) {
consola.error('rm_file Error', error.message)
socket.emit('sftp_error', error.message)
}
})
// socket.on('down_dir', async (path) => {
// const exists = await sftpClient.exists(path)
// if(!exists) return socket.emit('not_exists_dir', '文件夹不存在或当前不可访问')
// let res = await sftpClient.downloadDir(path, sftpCacheDir)
// socket.emit('down_dir_success', res)
// })
socket.on('down_file', async ({ path, name, size, target = 'down' }) => {
// target: down or preview
const exists = await sftpClient.exists(path)
if(!exists) return socket.emit('not_exists_dir', '文件不存在或当前不可访问')
try {
const localPath = rawPath.join(sftpCacheDir, name)
let timer = null
let res = await sftpClient.fastGet(path, localPath, {
step: step => {
if(timer) return
timer = setTimeout(() => {
const percent = Math.ceil((step / size) * 100) // 下载进度为服务器下载到服务端的进度,前端无需*2
console.log(`从服务器下载进度:${ percent }%`)
socket.emit('down_file_progress', percent)
timer = null
}, 200)
}
})
consola.success('sftp下载成功: ', res)
let buffer = fs.readFileSync(localPath)
let data = { buffer, name }
switch(target) {
case 'down':
socket.emit('down_file_success', data)
break
case 'preview':
socket.emit('preview_file_success', data)
break
}
fs.unlinkSync(localPath) //删除文件
} catch (error) {
consola.error('down_file Error', error.message)
socket.emit('sftp_error', error.message)
}
})
socket.on('up_file', async ({ 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 {
const localPath = rawPath.join(sftpCacheDir, name)
fs.writeFileSync(localPath, file)
let res = await sftpClient.fastPut(localPath, fullPath)
consola.success('sftp上传成功: ', res)
socket.emit('up_file_success', res)
} catch (error) {
consola.error('up_file Error', error.message)
socket.emit('sftp_error', error.message)
}
})
/** 分片上传 */
// 1. 创建本地缓存文件夹
let md5List = []
socket.on('create_cache_dir', async ({ targetPath, name }) => {
// console.log({ targetPath, name })
const exists = await sftpClient.exists(targetPath)
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================')
socket.emit('create_cache_success')
})
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)
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)
const resultFilePath = rawPath.join(sftpCacheDir, name, name)
try {
console.log('md5List: ', md5List)
const arr = md5List.map((chunkFilePath, index) => {
return pipeStream(
chunkFilePath,
// 指定位置创建可写流
fs.createWriteStream(resultFilePath, {
start: index * range,
end: (index + 1) * range
})
)
})
md5List = []
await Promise.all(arr)
let timer = null
let res = await sftpClient.fastPut(resultFilePath, fullPath, {
step: step => {
if(timer) return
timer = setTimeout(() => {
const percent = Math.ceil((step / size) * 100)
console.log(`上传服务器进度:${ percent }%`)
socket.emit('up_file_progress', percent)
timer = null
}, 200)
}
})
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) // 传服务器后移除文件夹及其文件
}
})
}
module.exports = (httpServer) => {
const serverIo = new Server(httpServer, {
path: '/sftp',
cors: {
origin: '*'
}
})
serverIo.on('connection', (socket) => {
// 前者兼容nginx反代, 后者兼容nodejs自身服务
let clientIp = socket.handshake.headers['x-forwarded-for'] || socket.handshake.address
let sftpClient = new SFTPClient()
consola.success('terminal websocket 已连接')
socket.on('create', ({ host: ip, token }) => {
const { code } = verifyAuth(token, clientIp)
if(code !== 1) {
socket.emit('token_verify_fail')
socket.disconnect()
return
}
const sshRecord = readSSHRecord()
let loginInfo = sshRecord.find(item => item.host === ip)
if(!sshRecord.some(item => item.host === ip)) return socket.emit('create_fail', `未找到【${ ip }】凭证`)
let { type, host, port, username, randomKey } = loginInfo
// 解密放到try里面防止报错【公私钥必须配对, 否则需要重新添加服务器密钥】
randomKey = AESDecrypt(randomKey) // 先对称解密key
randomKey = RSADecrypt(randomKey) // 再非对称解密key
loginInfo[type] = AESDecrypt(loginInfo[type], randomKey) // 对称解密ssh密钥
consola.info('准备连接Sftp', host)
const authInfo = { host, port, username, [type]: loginInfo[type] }
sftpClient.connect(authInfo)
.then(() => {
consola.success('连接Sftp成功', host)
return sftpClient.list('/')
})
.then((rootLs) => {
// 普通文件-、目录文件d、链接文件l
socket.emit('root_ls', rootLs) // 先返回根目录
listenInput(sftpClient, socket) // 监听前端请求
})
.catch((err) => {
consola.error('创建Sftp失败:', err.message)
socket.emit('create_fail', err.message)
})
})
socket.on('disconnect', async () => {
sftpClient.end()
.then(() => {
consola.info('sftp连接断开')
})
.catch((error) => {
consola.info('sftp断开连接失败:', error.message)
})
.finally(() => {
sftpClient = null
const cacheDir = rawPath.join(sftpCacheDir)
clearDir(cacheDir)
})
})
})
}