diff --git a/LICENSE b/LICENSE index 595d2e6..6b0017e 100644 --- a/LICENSE +++ b/LICENSE @@ -1,21 +1,37 @@ -MIT License +Business Source License 1.1 -Copyright (c) 2017 Sunil Wang +Licensor: Chaos Zhu +Licensed Work: EasyNode (WebSSH & WebSFTP Panel) +The Licensed Work is © 2025 Chaos Zhu -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: +License Grant: +Under this license, you are granted the right to use, copy, modify, and distribute the Licensed Work, subject to the Usage Limitations below, until the Change Date. -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. +Usage Limitations: +Free use is permitted only for: +- Personal, non-commercial purposes +- Educational and research purposes +- Internal evaluation or testing -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. +Commercial use, including but not limited to: +- Providing the Licensed Work as a hosted service (SaaS) +- Selling, licensing, or charging for access +requires a separate commercial agreement with the Licensor. + +Prohibited Uses: +Without a commercial license, you may not: +- Use the Licensed Work for any commercial purpose +- Remove or alter copyright, license notices, or trademarks +- Rebrand or redistribute as your own product + +Change Date: +1 January 2035 + +Change License: +On the Change Date, the Licensed Work will be made available under the Apache License 2.0. + +Disclaimer: +THE LICENSED WORK IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, AND NONINFRINGEMENT. + +Limitation of Liability: +IN NO EVENT SHALL THE LICENSOR BE LIABLE FOR ANY CLAIM, DAMAGES, OR OTHER LIABILITY ARISING FROM THE USE OF THE LICENSED WORK. diff --git a/server/app/config/index.js b/server/app/config/index.js index 62e97de..2f89142 100644 --- a/server/app/config/index.js +++ b/server/app/config/index.js @@ -22,6 +22,7 @@ module.exports = { chatHistoryDBPath: path.join(process.cwd(),'app/db/chat-history.db'), favoriteSftpDBPath: path.join(process.cwd(),'app/db/favorite-sftp.db'), proxyDBPath: path.join(process.cwd(),'app/db/proxy.db'), + fileTransferDBPath: path.join(process.cwd(),'app/db/file-transfer.db'), apiPrefix: '/api/v1', logConfig: { outDir: path.join(process.cwd(),'./app/db/logs'), diff --git a/server/app/controller/plus.js b/server/app/controller/plus.js index 253fdf2..7b91626 100644 --- a/server/app/controller/plus.js +++ b/server/app/controller/plus.js @@ -1 +1 @@ -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 \ No newline at end of file +U2FsdGVkX1+naZR7lVcI3HbApOZ76PM/cIFxxmTaWxIaY5rA6yB9Ttyt07Ywv1Mt7J8txrCT1Wk/9Tcrg382dipl7pEzL9vbAB5OJvUq4+5Ya/ysyIjZbX2bjiHqIRwt2FrYpeiETJNI6FhthqyEWEdvIw/cq0CUTZA6wLwhVYa0X+nZfs0guBUrDxaT4oJN2A2poQZ6kaHyy8IXzz/aUh2rXSpaIA9cAbOjQJA+cho/B3ayOvlF+UJcIEqFolHW17iqWO/88r3efht5SLJg63KoegT8uR1zjweu0PdiOom1w6QBqdRs0PBmKvPpnAEXqY51Ucx8NoFRzpGjZGbTR4wwWmc8PxFzb2KEVdYoI/m/3utY4snZMHC2zZgARNDysqqJ9gSXr3VshH5VayWv4H+Dz2JEK83RChvQwxLnhYmEbceZs76umKFDRU2dFiQA6hfh0edfzmQQ9uBMxRo8e4BGM1XuIJkj53mbXoir4MYck3BoL1oXtnk8j8r15nXSVHoY2mYZpawS0VVTGYCmNPE8RXiPAd6A9fmvU1uLM3HHEd9Vro5yd7AWhojk7zive4uA3y5hO0202yrGOpiDQ0g0Q6NSY35Bvor0rc5Q2ocBWpyBbyYrPRUw6f8WqQwacPJYjzWiGDcTK910tUf+sU8p/670qWc1Vg42cgX1W4cKFICFpAquAcDeI74HDAc9O64Fhnsgj5+qIBF+tCju+1M1GhqG8fBy7XYF52pwhBGOf0o0LN2XECGZUhMp6hmMgDIhxzUfKCnlZbjlnU4U9Hm00ZyEpF9wODDYvAoxK9YicIVCzXX6cVTSlyoYuhE9kJgSDpqx27kREgNhI01BMEuarH7LGP6nJmybOCyV57xmaMKt59Ivn6cyDF4OxLnOlX/5E0WKBdHTVOCKbEkhHBUSHMVJ7YprTyIjJXJGwfAuvITFcdNKCx7mEzigBxYrdz1wUvM9VPPB/ydRQGIfjrhoaEnEO8lt7TjeVLEs44HM2wFOlHB0VOxVCoFUZacMpRLgmG4aOtFy3NNXy3SFwxSTCgLg3tDajeLxpu52vbFHugMZS7ZYwf9Z2HBmnHLc6vvqZXgrjEL/KpTiKb6MEMmLxSG8+MFp3OXQO49YP2QqvOu7LFGCZSYxzUGjLwoZmHQNspKwH8qzjs4YQbN65JbcdRMFl9WTL3odY99zA3ko9/FZc0OaDo0yOYS3MG73rGHuysgnhOfhFDGLYW/dNATcaoMerSCkKssz94mykJcQO0680Aj7XoloCiOuQ0MVC6d8PQ1+pij9AVhz1axsYZR852cXTs3xOBIhTv4aRb5vCZKP18TyZXjxBUsDs09HUMZwvsmbPcRMf0b4xArAHdyWJ2IZRKWcDBsvNJRGi43tz7IMt6C9ifapuESWCLV5alyrTCz0bhK0F2kKGYDKKJdXzRdsMS/N2f06gRzkwsa7gtEpTAM1vQrSCNcqjt0U1Wfj3mjWTahe0O7rEJrDN0Tm6/fOh5N9+VGL0KiemPIWYHGSegE8tPM+2Ku2lBjNdZBOAO08EQe310aJdu/6IASvNRKMbMTN053pr+GM2t8cF75Q3yUu6KSCJ5ro0ByN/+rMl86Wl35h03Ur0Suxrkzhw4h1iSvDYAf84+RTq3CYajrguykBtizwAP4CXtxHpRGfZDFnnkAyxJpTz6qtpGrKZHOTdPg115P1oqK+Nh2tE72rutBocrLrDyUd3ZJBZceLUWoy0kiLJQAK/3OfAjTMXYaW0xzxg7kpAepBhzX59MYMKekiMf8IJ/4PrLkPoAESDzpghzXfyIuZz6NmmDnjT++/pdlkIWGjST5MJBNZvCni1eb0Po0RHH+trsKcTqwVU7feIu1zH+aI1IuoxaETd/QGZp9ogzcs6FyISqxBPohsqU3QeDn2nRF1SbrBEmMTwejRXCRCIsLGvqgL+NgJmxmCh36O1gH3Lf9ytlABumMJ0yCgNcHzy+FTMBhNJ/VtlLCvrA6q8Dg3v1PB1/+XCedOjEaUV3xKIqKdvwt5Ki0WFzXpFIN4BX3yQdBLfxH4TceBj2sIW5+qi08MNi6fKYuoZk69LO+CMXoNaTHVqxxLHVYP3mM70Tqy1YWExyXoY+7QHMVO0YWO0gDOAKXCMoPsB5H+qKzbkF0elDydLM8TocHNE0/8jH48biuKPQaiKqDQxV4GnlG7OgnfyQ0iM7fGIR1s17AkbaK/lrr/rGDLqGuiqgLofCbVqMYsIUEJORLWX3eB7Ooj42gokvhVuM0cFc1xxMpadfOE6K+wMIVWtlLaIX6m/0R2qgZaEbmvq9YAah81YMugvFgVgUpoqmRIOedYaszEzD3U12XzG4XhxNBNgPT/D/HyMPLbaW++93m+Z8aVTHDiI/auJyPrqbQDnxgwCqDGjjblXeQ5T0QnO0+ASzHIpSjtU2xDUvy4rP+/f4OQbe1mqC/Iyb14SPRcYPm7Lk9q8pAmL9OGLvn0tpNbgNANiZZjSyl9MVss/Al6kN0uxGxfxCefICY0CIa8DVT1r1eHS/m/9T7pxbBmQw3QJnSQyFLJbyClGAkieHSCx05kTYgbYDktbb44a2pfzQZQcSzNSBE7YgKCW1kfkEaWHhhfNERumdtwddYGoR4HB/IF+nHVH2zcMnVU7NkKafdd/UumFqF3xg1DvlTMCFaQlrXNLtBKefI43Y6B936WjQaAIq33W/xBbE9l8rtBx0IGt+DEBb23JD0CpNjqRgthjUde6sZ7F/MK9zqP/sImAJj4Gi8ONjaXaXiFp8nxu+xj0CDgjOOzE7UG0bWhVHQhEpjdkMTu+B+b3eFPeCWQLFV77+Ildey5jxyHSYZKReX6LdQ8+Hk+QtG5/kzbjziltVeQ35zKoZydSjBrmpOKbwdKdvi+BTxyeCbTc1sLAuTDlxGJXzlNSMR3d94OvIj6NkJCAFx0hgMYu1DfVK3QqxFDeS0yLA/x/9t2/T88aXnziXprDa/DHBHgqqjuwE45XVVUgwkpc+o1kyEEfbpHSc+7JhGX7Xd4577yyJf8KuBS7hup0yu+wPEEDT8Biqr22nKa0vqRHL44nkWj98dz04d8zr708wduw5X6N15iCkjDGxR9H7maN+6gj8ApPamCCLZew5TIpZVUW5OdNeNhfu4785dIIjas7muCOaTTMurN4SwMFAp4Y6yRtYv5pCGv+fYxvmU+rLbkm0A1OIyLzOj3/fkb/v6Iur/TYKARByTKpdzKVUr7ULsDLENXS8qWJ8bn9n4XnjHgkt01mEs/bso2uXexYTtd1IzAoOi1wiMxypgxT4hITk+DW2uoB8m+6PsecAdH7xYJcuyY4hBID9Dtreu5pa0YwJGfEtP9tz1Ack5yjXuZGYpkzTFPjK+KlY07KDq2zYoXA+78+Pzu7J7ZBk3ozudQpNExEJGuJbE0qlU8AdKCs8V6gz3+ctDrjQ3Xb5cwzQhXwgqDeQtb5+wXIV8GALTsu9rzVQaXIbQ1aBdySjmspQfBlja3gm5PVblxG8iTzj6q8wlcJJccb/Kat2uSw2A1fpx1oiNAgshErcIN6QygrSE6XIrRDoaZwGwrR134XNeQkFBZNmssJBN62DDSylCHjSZtfty3P3WGxkvF3uWxHZ5KH1+kGOttrWkXhcIqXn6sYeJ0E0hT7CoED9kcWQO9nyM1KVodld+wb3KEu1lSFrLTNoTwMKJI3u8/RiZjj6lfbsZJ7yJU1bBm3ya0FS0M8IBFSNtu3RD+lM4HnfFpfbPuPloV3q/xwMPm7Oe3U27veBp2P85/eLdnC9j93NZ+j6pMU77JcUv+KNU7vo08TfvkF+Jo9FxM3cjXGib5lJlgewOzY9eHhm40mmwRlPBB0GvW40QXepj1J9jBCNbPQxsWL7i6VcpCUMN3ZfH5aoxRdiVmmpDwwkrqfzqO/o45vD9Q0WnBHmwIb/QRbpIa1FYLE9fyOX9R1HnEC2z8hTLMMtZbEgK/5JIzsjkcPGCFc/bm7WwaAn8r3kdjoNtHeqMvITDzGm6Lnqyio82zp80+PMtuaXRc2hE9s56XudKc6btj7PLxK4LMfnWKEN/+uPhJu9jNFogbJVCvhr/iSerg9Els4Xf1KUMp7NknVC5m0SnuM6SVQUMHWkrBp659fpwpwDcErrFj7lPlC6rOT1xjCvkEKwJwnLr+UyM9IFQsF91CZ26YXJQZ61n3/PXP4kzAzlQrOq+pbuV+HdoPk54Khc5zqO778dTX+3uZOYf3dJZuKOU2FgE+TlBqHQVSkenEJWXY+YKkh8jNNt0DxCyl8YkkUJ2l5R6x9lvNUSfi+8pCwWpzNl2H9u8Skxb9L3/G0N0fXLb78wgpYP9dHqlEz/+L5Ex/Tim50okcLz6IbRJDTl7AI3jrmncTlRrTdFhZ0nQQ6VZxNFLC263z9zMzDxfRoLUzfJXJ/qgoi7AzYUCmwdKUHljR3oxTt6rvX85+Fz7wSRD+NmZVbWx5g18uLGT+ufQNuXvuy/D3f2QnT14TrY1aCnIGZXRNiipICGS44mlQ5UO94L99ngyE2UaAUhI4a3NOChLOyaFvXLc47FPH3tRRQX4ceztgdwgL36zh9hYXTpdiPdDm+SHoBGeQNHLnyjCg+30+AalLjj5Ojz+fJ0sBYj4S2GjRmWhMdbqerO7WJIjMCF9GWosLuZl7ocrtc80KVJ0grZvS04hcR5EflvpZJb1MTfo99HHpiC7Lo4dsgoBOXCUVMbGbAEVcBjwIbpABxXpTgl/ExWFKi7/pPd5u4uvmSrnqkLqlzXDQjG8AQYyDsOoglEPFFGz1oOYJRIIIgExsRpZDGsATu7o2JUxNi+B3eeEKiavu1zVpBVAZVMJlGPF/Ac+0o92OUod/zTOUW68DGUMHT3r7MYapq+mFVAkQKWXlBs1gxrlUKZGcf1zlzcI6e98FNVcVqSXzwU2FWQa8ULgbDjpzvNwrVlLxkC1GSeaXTV6cEhftHZgK1Dn9v3BJGIrD8E/DhHWTZs02o55uM3N15Glb+zjUwfDD0H1Kq2NOawQ9lRDXBolctJn4UxtqK5YMincYqLv5F7rhJRdycBLhHW8vxtwf5/kFmC7w/ETihJNS0hJpcvPmWybz4I9iCi9nWn9ySiKBIIfqVb5E9w9/obB1ZsqZnnnjyHxnXi1j9ipdYS0pgh+S3qhFtuz5kT4cL9M92mlhsI3eW1VnL8098tnKo63+sOa1qrRo/NMlBoVpjvp12/TkXtdDR5UiKaSib7pd8wDrwntvTSuWDZspStszlp4qmGlT7WtSaYd6OUyiHQEbVMDLUeyjKxijR4wCjPIhmMXZ+c9i6YJTwL23bemV+243L/zmOoLDK9XZ0sJKK3l2e4bW3Td8DHnsS4DzGDyejnBAzJgLfHiilQNclW9lrFafuOCV/upLv7oZRG6AssgLTRKf3EaHQJw6j2gHhf/XRmCODpMSH0t5ZjIvOrhPSYMcHh2rYMEO4U0UBPINFGGzQp4nb+JhK1Yy/ahCDNv5HsAKZffO9Z4EhKifYb6PP6K8cIXrnKFqfK99NJygxXfQEqxsay5wDimJrtaDOcEQFSY7EE8mjXXLRSQyxy6reE23RedSMsNrksM/2X1U5/UQN4n/jUtkPrs3UR8D+NA4/1e0BQpkVPmxAES8GbsqoRZwy8sUU/KmM+BuJRV727llfjClq6VVBS4UDJtnEqg1Zr14i3VXZG68VQ7SIO+4RjnyR/Vju/nuSLiHotlYrzAa520DCYZghPie7VV9whp9f+gHbpcfYW5QQgok6Ei7DPZueQxjW0KCUV+4gSVVCTl9fv+3bB0w2mmPD01qo/uF1U+uxTqGDJht2nAntPxuKFcxpUF4JeJ9rgdOohxpyHol+dNtKlKhpn/q84hW8H00YdG7YzSFVMwHtUQn2Ku4ffi4kTC0XqhpSUfYeynWOknGDmEl0uNu/4WKTrBc1lEsDjF1Yb97AOC3hGOGiq4nIKWZPXtBFlaXKLoZVuWpaiVNEL0EBIecHqndGCWS5rdiPeAKyxNQQIykIn8uoCbX9XLWdjJct8FjjJrcHj4hfmnzvApE5LC8zQ729msbykcWkqE8iuAxbg5TmMVIWoYcUzbpONTurTsf4OsGNzcMxfOYTACcQJI8eleToNpzpCPPdaf6WJ+lY4bKUBmmZRCajJ6lMSbcZgmVJfiR5ALTNrOn+voyvai+fgwoCrgsWMPA67J2AeFvUe+bZ8t3tzFeVjEfJMAQqWSuCXyWpss5HaFtpQSCM4Xck+YSaAyxng2626faw48c4UtXbU+k+oh+tMYvDg/A9MtrThyU49puM65yTbzptAZkdQGBeQnEvMrqY2zRwNGKrs554x6ttOd/Vn2UL01kQxurpzGHuD0QKuN2sY5fTWATar0aMjicmIhzIZNwfj0V5j/mHZx4J7vXHaN1s2IrA91I+F+0hFmluaNIeE41XzyKSikblJl/J2HTek7NOgkNa1iahAY0JDFG8m65nqE8LvLOpaB9/7AOFOfFeESJnLYTnpffQT5bB5NkPlEpOcq/v7Y8qBz6zmOFtN9fIj/4YrIcuWytEPjgyEi5KHiwuB7WdXFJxebZrcTxXnQmOJJN3fOY71Fj78KJM7lpYvLnAoj8pjSa9lsggPDaxkDlqAYe19v7ONvZ7byAmt9dPFSxbOQhMIIJ9xl/Cxfg8xi/jq4zLl+qc8wqI0CVybgWJM47MYDRwtQb12Xl \ No newline at end of file diff --git a/server/app/server.js b/server/app/server.js index 6942e1e..e66b6cf 100644 --- a/server/app/server.js +++ b/server/app/server.js @@ -8,6 +8,7 @@ const wsSftpV2 = require('./socket/sftp-v2') const wsDocker = require('./socket/docker') const wsOnekey = require('./socket/onekey') const wsServerStatus = require('./socket/server-status') +const wsFileTransfer = require('./socket/file-transfer') const { throwError } = require('./utils/tools') const httpServer = () => { @@ -28,6 +29,7 @@ function serverHandler(app, server) { wsDocker(server) // docker wsOnekey(server) // 一键指令 wsServerStatus(server) // 服务器状态监控 + wsFileTransfer(server) // 文件传输 app.context.throwError = throwError // 常用方法挂载全局ctx上 app.use(compose(middlewares)) // 捕获error.js模块抛出的服务错误 diff --git a/server/app/socket/file-transfer.js b/server/app/socket/file-transfer.js new file mode 100644 index 0000000..959985d --- /dev/null +++ b/server/app/socket/file-transfer.js @@ -0,0 +1,989 @@ +const path = require('path') +const { Server } = require('socket.io') +const { Client: SSHClient } = require('ssh2') +const consola = require('consola') +const { verifyAuthSync } = require('../utils/verify-auth') +const { isAllowedIp, fileTransferThrottle } = require('../utils/tools') +const { getConnectionOptions } = require('./terminal') +const { FileTransferDB, HostListDB } = require('../utils/db-class') +const decryptAndExecuteAsync = require('../utils/decrypt-file') + +const fileTransferDB = new FileTransferDB().getInstance() +const hostListDB = new HostListDB().getInstance() + +// 全局传输任务管理 +const activeTasks = new Map() // taskId -> { process, sshClient, status, ... } + +// 任务排序函数:运行中的任务优先,然后按updateTime降序 +function sortTasks(tasks) { + return tasks.sort((a, b) => { + // 状态优先级:running > 其他状态 + const statusPriority = { running: 0 } + const aPriority = statusPriority[a.status] ?? 1 + const bPriority = statusPriority[b.status] ?? 1 + + if (aPriority !== bPriority) { + return aPriority - bPriority + } + + // 相同状态按updateTime降序 + return (b.updateTime || 0) - (a.updateTime || 0) + }) +} + +// 获取排序后的任务列表(合并数据库和内存状态) +async function getSortedTasksList() { + const tasks = await fileTransferDB.findAsync({}, { sort: { updateTime: -1 } }) + + // 合并内存中的活跃任务状态 + const tasksWithStatus = tasks.map(task => { + const activeTask = activeTasks.get(task.taskId) + if (activeTask) { + return { + ...task, + status: activeTask.status, + progress: activeTask.progress, + speed: activeTask.speed, + eta: activeTask.eta, + errorMessage: activeTask.errorMessage + } + } + return task + }) + + // 按状态和时间重新排序 + return sortTasks(tasksWithStatus) +} + +module.exports = (httpServer) => { + const transferIo = new Server(httpServer, { + path: '/file-transfer', + cors: { origin: '*' } + }) + + let connectionCount = 0 + const connectedSockets = new Set() // 跟踪所有连接的socket + + transferIo.on('connection', (socket) => { + connectionCount++ + connectedSockets.add(socket) + consola.success(`file-transfer websocket 已连接 - 当前连接数: ${ connectionCount }`) + + // IP白名单检查 + let requestIP = socket.handshake.headers['x-forwarded-for'] || socket.handshake.address + if (!isAllowedIp(requestIP)) { + socket.emit('ip_forbidden', 'IP地址不在白名单中') + socket.disconnect() + return + } + + // 连接时发送当前所有任务状态 + socket.on('get_tasks', async ({ token }) => { + const { code } = await verifyAuthSync(token, requestIP) + if (code !== 1) { + socket.emit('token_verify_fail') + return + } + + try { + const sortedTasks = await getSortedTasksList() + socket.emit('tasks_list', sortedTasks) + + // 检查是否有运行中的任务,如果有则启动定时推送 + const runningTasks = sortedTasks.filter(task => task.status === 'running') + if (runningTasks.length > 0) { + startProgressBroadcast(socket, token, requestIP) + } + } catch (error) { + socket.emit('error', { message: '获取任务列表失败', error: error.message }) + } + }) + + // 启动传输任务 + socket.on('start_transfer', async (transferConfig) => { + const { token } = transferConfig + const { code } = await verifyAuthSync(token, requestIP) + if (code !== 1) { + socket.emit('token_verify_fail') + return + } + + try { + const { createTransferTask = null } = (await decryptAndExecuteAsync(path.join(__dirname, 'plus.js'))) || {} + if (!createTransferTask) throw new Error('Plus功能解锁失败: createTransferTask') + const task = await createTransferTask(transferConfig, socket, hostListDB, fileTransferDB, executeTransfer) + socket.emit('task_started', { taskId: task.taskId, message: '传输任务已启动' }) + + // 广播更新的任务列表 + const updatedTasks = await getSortedTasksList() + socket.emit('tasks_list', updatedTasks) + } catch (error) { + consola.error('启动传输任务失败:', error) + socket.emit('task_failed', { + taskId: transferConfig.taskId, + message: error.message + }) + } + }) + + // 取消任务 + socket.on('cancel_task', async ({ taskId, token }) => { + const { code } = await verifyAuthSync(token, requestIP) + if (code !== 1) { + socket.emit('token_verify_fail') + return + } + + try { + await cancelTransferTask(taskId) + socket.emit('task_cancelled', { taskId, message: '任务已取消' }) + } catch (error) { + socket.emit('error', { message: '取消任务失败', error: error.message }) + } + }) + + // 重试任务 + socket.on('retry_task', async ({ taskId, token }) => { + const { code } = await verifyAuthSync(token, requestIP) + if (code !== 1) { + socket.emit('token_verify_fail') + return + } + + try { + const task = await fileTransferDB.findOneAsync({ taskId }) + if (!task) { + throw new Error('任务不存在') + } + + // 重置任务状态 + await fileTransferDB.updateAsync( + { taskId }, + { + $set: { + status: 'running', + progress: 0, + speed: 0, + errorMessage: null, + updateTime: Date.now() + } + } + ) + + // 重新启动任务(不创建新任务,重用现有任务) + executeTransfer(task, socket) + socket.emit('task_started', { taskId: task.taskId, message: '任务重试中' }) + } catch (error) { + socket.emit('error', { message: '重试任务失败', error: error.message }) + } + }) + + // 删除单个任务 + socket.on('delete_task', async ({ taskId, token }) => { + const { code } = await verifyAuthSync(token, requestIP) + if (code !== 1) { + socket.emit('token_verify_fail') + return + } + + try { + const task = await fileTransferDB.findOneAsync({ taskId }) + if (!task) { + throw new Error('任务不存在') + } + + // 检查任务状态,不允许删除正在进行的任务 + if (task.status === 'running') { + throw new Error('无法删除正在进行的任务,请先取消任务') + } + + // 从数据库删除任务 + await fileTransferDB.removeAsync({ taskId }) + + socket.emit('task_deleted', { taskId, message: '任务已删除' }) + + // 广播任务列表更新 + const updatedTasks = await getSortedTasksList() + socket.emit('tasks_list', updatedTasks) + } catch (error) { + socket.emit('error', { message: '删除任务失败', error: error.message }) + } + }) + + // 清空已完成任务 + socket.on('clear_completed_tasks', async ({ token }) => { + const { code } = await verifyAuthSync(token, requestIP) + if (code !== 1) { + socket.emit('token_verify_fail') + return + } + + try { + // 删除所有已完成、失败、取消的任务 + const result = await fileTransferDB.removeAsync( + { + status: { $in: ['completed', 'failed', 'cancelled'] } + }, + { multi: true } + ) + + socket.emit('tasks_cleared', { + count: result, + message: `已清空 ${ result } 个任务` + }) + + // 广播任务列表更新 + const updatedTasks = await getSortedTasksList() + socket.emit('tasks_list', updatedTasks) + } catch (error) { + socket.emit('error', { message: '清空任务失败', error: error.message }) + } + }) + + socket.on('disconnect', () => { + connectionCount-- + connectedSockets.delete(socket) + + // 清理该socket的进度广播定时器 + stopProgressBroadcast(socket) + consola.info(`file-transfer websocket 断开连接 - 当前连接数: ${ connectionCount }`) + }) + }) + + return transferIo +} + +// 执行传输任务 +async function executeTransfer(taskData, socket) { + const { taskId } = taskData + + try { + // 更新任务状态为运行中,确保updateTime是最新的 + await updateTaskStatus(taskId, 'running', socket) + + // 获取源主机连接配置 + const sourceOptions = await getConnectionOptions(taskData.sourceHostId) + + // 建立SSH连接到源主机 + const { sshClient } = await connectToHost(sourceOptions) + + // 注册活跃任务 + activeTasks.set(taskId, { + sshClient, + status: 'running', + progress: 0, + speed: 0, + startTime: Date.now(), + keyFile: null, // 用于跟踪临时密钥文件 + totalFiles: taskData.sourcePaths.length, // 初始化文件总数 + sourcePaths: taskData.sourcePaths // 保存源文件路径信息 + }) + + // 只支持Rsync传输 + if (taskData.method === 'rsync') { + await executeRsyncTransfer(taskData, sshClient, socket) + } else { + throw new Error(`不支持的传输方法: ${ taskData.method },仅支持 rsync`) + } + + // 传输完成 + await updateTaskStatus(taskId, 'completed', socket) + + } catch (error) { + consola.error(`传输任务 ${ taskId } 失败:`, error) + await updateTaskStatus(taskId, 'failed', socket, error.message) + } finally { + // 清理资源 + const activeTask = activeTasks.get(taskId) + if (activeTask) { + if (activeTask.sshClient) { + activeTask.sshClient.end() + } + activeTasks.delete(taskId) + } + } +} + +// Rsync传输实现 +async function executeRsyncTransfer(taskData, sshClient, socket) { + const { taskId, sourcePaths, targetPath } = taskData + + // 获取目标主机信息 + const targetConnectionData = await getConnectionOptions(taskData.targetHostId) + const targetOptions = targetConnectionData.authInfo + const targetHostAuthType = targetOptions.password ? 'password' : 'privateKey' + + consola.info(`目标主机认证方式: ${ targetHostAuthType }`) + + // 构建Rsync命令 + let rsyncCmd = [] + let envVars = {} + + // ssh密钥tmp路径 + let keyFile = null + + // 如果目标主机使用密码认证,源主机需使用sshpass + if (targetHostAuthType === 'password') { + // 检查源主机sshpass是否可用 + try { + await checkSshpassAvailable(sshClient) + // 使用环境变量方式传递密码,避免命令行参数解析问题 + envVars.SSHPASS = targetOptions.password + rsyncCmd.push('sshpass', '-e') // -e 表示从环境变量读取密码 + } catch (error) { + throw new Error('源主机未安装sshpass工具,无法进行密码认证传输。请使用密钥认证或在源主机安装sshpass: apt-get install sshpass 或 yum install sshpass') + } + } else { + keyFile = await createRemoteTempKeyFile(sshClient, targetOptions.privateKey) + } + + rsyncCmd.push('rsync', '-avz', '--progress', '--partial') // 归档、详细、压缩、进度、支持断点续传 + + // 添加增量同步和安全选项 + rsyncCmd.push('--inplace', '--append') // 断点续传关键选项 + + // 添加更详细的进度输出选项 + rsyncCmd.push('--stats', '--human-readable', '--itemize-changes') // 统计信息、可读格式、详细变更 + + // 构建SSH命令选项 + const sshOptions = [ + '-p', (targetOptions.port || 22).toString(), + '-o', 'StrictHostKeyChecking=no', + '-o', 'UserKnownHostsFile=/dev/null', + '-o', 'GlobalKnownHostsFile=/dev/null' + ] + + // 根据认证类型设置不同的SSH选项 + if (targetOptions.password) { + sshOptions.push('-o', 'PreferredAuthentications=password') + } else { + sshOptions.push('-o', 'BatchMode=yes') + } + + if (keyFile) { + sshOptions.push('-i', `"${ keyFile }"`) + // 记录临时密钥文件路径到活跃任务中 + const activeTask = activeTasks.get(taskId) + if (activeTask) { + activeTask.keyFile = keyFile + } + } + + // 封装成一个整体的 SSH 命令,放到双引号内,确保 rsync 正确解析 + const sshCmd = `ssh ${ sshOptions.join(' ') } -o LogLevel=ERROR` + rsyncCmd.push('-e', `"${ sshCmd }"`) + + // 添加传输选项 + if (taskData.options.delete) { + rsyncCmd.push('--delete') + } + if (taskData.options.excludePatterns && taskData.options.excludePatterns.length > 0) { + taskData.options.excludePatterns.forEach(pattern => { + rsyncCmd.push('--exclude', pattern) + }) + } + + // 添加源和目标路径 + rsyncCmd.push(...sourcePaths.map(item => item.path)) + rsyncCmd.push(`${ targetOptions.username }@${ targetOptions.host }:"${ targetPath }"`) + + consola.info(`执行Rsync命令: ${ rsyncCmd.join(' ') }`) + if (Object.keys(envVars).length > 0) { + consola.info(`环境变量: ${ Object.keys(envVars).join(', ') }`) + } + + return new Promise((resolve, reject) => { + // 在源主机上执行Rsync命令 + let finalCommand = rsyncCmd.join(' ') + + // 环境变量,需要在命令前设置 + if (Object.keys(envVars).length > 0) { + const envString = Object.entries(envVars) + .map(([key, value]) => `${ key }='${ value.replace(/'/g, '\'"\'"\'') }'`) + .join(' ') + finalCommand = `${ envString } ${ finalCommand }` + } + + // consola.info(`最终Rsync命令: ${ finalCommand }`) + let start = false + sshClient.exec(finalCommand, (err, stream) => { + if (err) { + reject(err) + return + } + + let errorOutput = '' + const activeTask = activeTasks.get(taskId) + + stream.on('close', async (code) => { + if (code === 0) { + // 传输成功,设置为校验状态 + const activeTask = activeTasks.get(taskId) + if (activeTask && activeTask.progressTracker) { + activeTask.progressTracker.isVerifying = true + await updateTaskProgress(taskId, activeTask.progressTracker, socket) + } + consola.success(`Rsync传输完成: ${ taskId }`) + resolve() + } else { + consola.error(`Rsync传输失败: ${ taskId }, 退出码: ${ code }`) + reject(new Error(`Rsync传输失败: ${ errorOutput || '未知错误' }`)) + } + }) + + stream.on('data', (data) => { + if (!start) { + start = true + // 清理密钥文件并从内存中移除 + if (activeTask.keyFile) { + cleanupRemoteKeyFile(activeTask.sshClient, activeTask.keyFile) + } + return + } + fileTransferThrottle(parseRsyncProgress(data.toString(), taskId, socket)) + }) + + stream.stderr.on('data', (data) => { + if (!start) { + start = true + // 清理密钥文件并从内存中移除 + if (activeTask.keyFile) { + cleanupRemoteKeyFile(activeTask.sshClient, activeTask.keyFile) + } + return + } + const output = data.toString() + errorOutput += output + consola.warn(`Rsync stderr: ${ output }`) + + // 解析错误信息中的进度信息 + fileTransferThrottle(parseRsyncProgress(output, taskId, socket)) + }) + + // 存储stream引用以支持取消操作 + if (activeTask) { + activeTask.stream = stream + } + }) + }) +} + +// 解析Rsync进度 +function parseRsyncProgress(output, taskId, socket) { + const activeTask = activeTasks.get(taskId) + if (!activeTask) return + + // 初始化进度跟踪器(如果不存在) + if (!activeTask.progressTracker) { + activeTask.progressTracker = { + totalFiles: activeTask.totalFiles || 1, + completedFiles: 0, + currentFile: null, + files: new Map(), // 文件路径 -> 进度信息 + overallProgress: 0, + isVerifying: false + } + } + + const tracker = activeTask.progressTracker + const outputLine = output.trim() + + // 添加调试日志 + consola.info(`Rsync输出 [${ taskId }]: "${ outputLine }"`) + + // 检测是否在校验阶段 + if (outputLine.includes('verifying') || + outputLine.includes('checking') || + outputLine.includes('delta-transmission disabled') || + (tracker.overallProgress >= 100 && outputLine.includes('receiving'))) { + if (!tracker.isVerifying) { + tracker.isVerifying = true + updateTaskProgress(taskId, tracker, socket) + } + return + } + + // 如果还没有当前文件,且总文件数为1,使用第一个源文件作为当前文件 + if (!tracker.currentFile && tracker.totalFiles === 1) { + const activeTask = activeTasks.get(taskId) + if (activeTask && activeTask.sourcePaths && activeTask.sourcePaths.length > 0) { + const sourcePath = activeTask.sourcePaths[0].path + const fileName = sourcePath.split('/').pop() + tracker.currentFile = fileName || sourcePath + if (!tracker.files.has(tracker.currentFile)) { + tracker.files.set(tracker.currentFile, { + progress: 0, + size: activeTask.sourcePaths[0].size || 0, + transferred: 0, + speed: 0, + status: 'transferring' + }) + } + consola.info(`单文件传输初始化: ${ tracker.currentFile }`) + } + } + + // 检测新文件开始传输 + // 模式1: itemize-changes 格式 - 0) { + // 找到最后一个未完成的文件作为当前文件 + for (const [fileName, fileInfo] of tracker.files.entries()) { + if (fileInfo.status === 'transferring' && fileInfo.progress < 100) { + tracker.currentFile = fileName + break + } + } + } + + // 更新当前文件进度 + if (tracker.currentFile) { + const fileInfo = tracker.files.get(tracker.currentFile) + if (fileInfo) { + fileInfo.progress = fileProgress + fileInfo.speed = speed + fileInfo.transferred = transferred + if (fileProgress === 100) { + fileInfo.status = 'completed' + // 文件完成后清除当前文件,为下一个文件做准备 + consola.info(`文件传输完成: ${ tracker.currentFile }`) + tracker.currentFile = null + } + } + } else { + // 如果仍然没有当前文件,记录警告但继续处理 + consola.warn(`收到进度信息但没有当前文件 [${ taskId }]: ${ fileProgress }%`) + } + } + + // 计算总体进度 + if (tracker.totalFiles > 0) { + if (tracker.totalFiles === 1) { + // 单文件传输:直接使用当前文件进度 + if (tracker.currentFile && tracker.files.has(tracker.currentFile)) { + const currentFileInfo = tracker.files.get(tracker.currentFile) + tracker.overallProgress = currentFileInfo ? currentFileInfo.progress : 0 + } else { + // 如果没有文件信息,使用解析出的fileProgress + tracker.overallProgress = fileProgress || 0 + } + } else { + // 多文件传输:基于已完成文件数 + 当前文件进度的总体进度 + let overallProgress = (tracker.completedFiles / tracker.totalFiles) * 100 + if (tracker.currentFile && tracker.files.has(tracker.currentFile)) { + const currentFileInfo = tracker.files.get(tracker.currentFile) + if (currentFileInfo && currentFileInfo.status !== 'completed') { + overallProgress += (currentFileInfo.progress / tracker.totalFiles) + } + } + tracker.overallProgress = Math.min(100, Math.round(overallProgress)) + } + } + + // 更新活跃任务状态 + activeTask.progress = tracker.overallProgress + activeTask.speed = speed + + updateTaskProgress(taskId, tracker, socket) +} + +// 更新任务进度 +async function updateTaskProgress(taskId, progressTracker, socket) { + try { + // 准备数据库更新数据 + const updateData = { + progress: progressTracker.overallProgress, + updateTime: Date.now() + } + + // 如果在校验阶段,添加状态 + if (progressTracker.isVerifying) { + updateData.status = 'verifying' + } + + await fileTransferDB.updateAsync( + { taskId }, + { $set: updateData } + ) + + // 准备发送给前端的详细进度数据 + const activeTask = activeTasks.get(taskId) + const progressData = { + taskId, + overallProgress: progressTracker.overallProgress, + completedFiles: progressTracker.completedFiles, + totalFiles: progressTracker.totalFiles, + currentFile: progressTracker.currentFile, + isVerifying: progressTracker.isVerifying, + speed: activeTask ? activeTask.speed : 0, + files: Array.from(progressTracker.files.entries()).map(([path, info]) => ({ + path, + progress: info.progress, + status: info.status, + speed: info.speed, + transferred: info.transferred + })) + } + + consola.info(`推送进度更新 [${ taskId }]:`, { + overall: progressData.overallProgress, + files: `${ progressData.completedFiles }/${ progressData.totalFiles }`, + current: progressData.currentFile, + speed: `${ (progressData.speed / 1024 / 1024).toFixed(1) }MB/s`, + verifying: progressData.isVerifying + }) + + // 发送给原始socket(如果存在且连接) + if (socket && socket.connected) { + socket.emit('task_progress', progressData) + } + } catch (error) { + consola.error('更新任务进度失败:', error) + } +} + +// 更新任务状态 +async function updateTaskStatus(taskId, status, socket, errorMessage = null) { + try { + const updateData = { + status, + updateTime: Date.now() + } + + if (errorMessage) { + updateData.errorMessage = errorMessage + } + + await fileTransferDB.updateAsync({ taskId }, { $set: updateData }) + + // 更新内存中的状态 + const activeTask = activeTasks.get(taskId) + if (activeTask) { + activeTask.status = status + if (errorMessage) { + activeTask.errorMessage = errorMessage + } + } + + // 通知前端 + socket.emit('task_status_changed', { + taskId, + status, + errorMessage + }) + } catch (error) { + consola.error('更新任务状态失败:', error) + } +} + +// 取消传输任务 +async function cancelTransferTask(taskId) { + const activeTask = activeTasks.get(taskId) + + if (activeTask) { + // 终止SSH连接 + if (activeTask.sshClient) { + activeTask.sshClient.end() + } + activeTasks.delete(taskId) + } + + // 更新数据库状态 + await fileTransferDB.updateAsync( + { taskId }, + { + $set: { + status: 'cancelled', + updateTime: Date.now() + } + } + ) +} + +// 连接到主机(文件传输专用,直连不走代理) +async function connectToHost(connectionOptions) { + return new Promise((resolve, reject) => { + const sshClient = new SSHClient() + + sshClient.on('ready', () => { + resolve({ sshClient }) + }) + + sshClient.on('error', (err) => { + reject(err) + }) + + // 直接连接,不使用代理 + sshClient.connect(connectionOptions.authInfo) + }) +} + +// 检查sshpass工具是否可用 +async function checkSshpassAvailable(sshClient) { + return new Promise((resolve, reject) => { + sshClient.exec('which sshpass', (err, stream) => { + if (err) { + reject(err) + return + } + + let output = '' + stream.on('close', (code) => { + if (code === 0 && output.trim()) { + resolve(true) + } else { + reject(new Error('sshpass not found')) + } + }) + + stream.on('data', (data) => { + output += data.toString() + }) + + stream.stderr.on('data', () => { + // 忽略stderr + }) + }) + }) +} + +// 在源主机上创建临时密钥文件 +async function createRemoteTempKeyFile(sshClient, privateKey) { + return new Promise((resolve, reject) => { + const remotePath = `/tmp/easynode_key_${ Date.now() }_${ Math.random().toString(36).slice(2) }` + sshClient.sftp((err, sftp) => { + if (err) return reject(err) + // 打开文件句柄 + sftp.open(remotePath, 'w', 0o600, (openErr, handle) => { + if (openErr) { + sftp.end() + return reject(openErr) + } + // 写入私钥内容(Buffer) + const keyBuffer = Buffer.from(privateKey, 'utf-8') + sftp.write(handle, keyBuffer, 0, keyBuffer.length, 0, (writeErr) => { + if (writeErr) { + sftp.close(handle, () => sftp.end()) + return reject(writeErr) + } + sftp.close(handle, (closeErr) => { + sftp.end() + if (closeErr) return reject(closeErr) + resolve(remotePath) + }) + }) + }) + }) + }) +} + +// 安全清理远程密钥文件 +function cleanupRemoteKeyFile(sshClient, keyFile) { + if (!keyFile || !sshClient) return + + consola.info(`清理远程密钥文件: ${ keyFile }`) + + // 先删除文件,再验证删除 + // const cleanupCmd = `rm -f "${ keyFile }" && if [ -f "${ keyFile }" ]; then echo "CLEANUP_FAILED"; else echo "CLEANUP_SUCCESS"; fi` + const cleanupCmd = 'cd /tmp && rm -f easynode_key_* && if ls easynode_key_* 2>/dev/null; then echo "CLEANUP_FAILED"; else echo "CLEANUP_SUCCESS"; fi' + sshClient.exec(cleanupCmd, (err, stream) => { + if (err) { + consola.error('清理密钥文件时SSH错误:', err) + return + } + + let output = '' + stream.on('data', (data) => { + output += data.toString() + }) + + stream.on('close', () => { + if (output.includes('CLEANUP_SUCCESS')) { + consola.success(`密钥文件清理成功: ${ keyFile }`) + } else if (output.includes('CLEANUP_FAILED')) { + consola.error(`密钥文件清理失败: ${ keyFile }`) + // 强制清理尝试 - 覆盖后删除 + const forceCleanup = `echo "" > "${ keyFile }" && rm -f "${ keyFile }"` + sshClient.exec(forceCleanup, () => { + consola.info(`强制清理密钥文件: ${ keyFile }`) + }) + } else { + consola.warn(`密钥文件清理状态未知: ${ keyFile }`) + } + }) + + stream.stderr.on('data', (data) => { + consola.warn('清理密钥文件stderr:', data.toString()) + }) + }) +} + +// 定时进度广播管理 +const progressBroadcastTimers = new Map() // socket -> timer + +// 启动进度广播 +function startProgressBroadcast(socket, token, requestIP) { + // 如果该socket已经有定时器,先清除 + if (progressBroadcastTimers.has(socket)) { + clearInterval(progressBroadcastTimers.get(socket)) + } + + // 启动新的定时器,每秒检查并推送运行中任务的进度 + const timer = setInterval(async () => { + try { + // 验证token是否仍然有效 + const { code } = await verifyAuthSync(token, requestIP) + if (code !== 1) { + // token失效,停止广播 + stopProgressBroadcast(socket) + return + } + + // 检查是否还有运行中的任务 + const runningTasks = Array.from(activeTasks.values()).filter(task => task.status === 'running') + if (runningTasks.length === 0) { + // 没有运行中的任务,停止广播 + consola.info('没有运行中的任务,停止进度广播') + stopProgressBroadcast(socket) + return + } + + // 推送所有运行中任务的最新进度 + for (const [taskId, activeTask] of activeTasks.entries()) { + if (activeTask.status === 'running' && activeTask.progressTracker) { + const progressData = { + taskId, + overallProgress: activeTask.progressTracker.overallProgress || 0, + completedFiles: activeTask.progressTracker.completedFiles || 0, + totalFiles: activeTask.progressTracker.totalFiles || 1, + currentFile: activeTask.progressTracker.currentFile, + isVerifying: activeTask.progressTracker.isVerifying || false, + speed: activeTask.speed || 0 + } + + // 检查socket是否仍然连接 + if (socket.connected) { + socket.emit('task_progress', progressData) + } else { + // socket已断开,停止广播 + stopProgressBroadcast(socket) + return + } + } + } + } catch (error) { + consola.error('进度广播出错:', error) + stopProgressBroadcast(socket) + } + }, 1500) + + progressBroadcastTimers.set(socket, timer) + consola.info('已启动进度广播定时器') +} + +// 停止进度广播 +function stopProgressBroadcast(socket) { + const timer = progressBroadcastTimers.get(socket) + if (timer) { + clearInterval(timer) + progressBroadcastTimers.delete(socket) + consola.info('已停止进度广播定时器') + } +} + diff --git a/server/app/socket/file.js b/server/app/socket/file.js deleted file mode 100644 index e69de29..0000000 diff --git a/server/app/socket/plus.js b/server/app/socket/plus.js index 88ee78d..04f1ffc 100644 --- a/server/app/socket/plus.js +++ b/server/app/socket/plus.js @@ -1 +1 @@ -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 \ No newline at end of file +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 \ No newline at end of file diff --git a/server/app/utils/db-class.js b/server/app/utils/db-class.js index 064509e..22d4895 100644 --- a/server/app/utils/db-class.js +++ b/server/app/utils/db-class.js @@ -14,7 +14,8 @@ const { aiConfigDBPath, chatHistoryDBPath, favoriteSftpDBPath, - proxyDBPath + proxyDBPath, + fileTransferDBPath } = require('../config') module.exports.KeyDB = class KeyDB { @@ -189,4 +190,15 @@ module.exports.ProxyDB = class ProxyDB { getInstance() { return ProxyDB.instance } +} + +module.exports.FileTransferDB = class FileTransferDB { + constructor() { + if (!FileTransferDB.instance) { + FileTransferDB.instance = new Datastore({ filename: fileTransferDBPath, autoload: true }) + } + } + getInstance() { + return FileTransferDB.instance + } } \ No newline at end of file diff --git a/server/app/utils/plus-server.js b/server/app/utils/plus-server.js index dc8354c..ea75cca 100644 --- a/server/app/utils/plus-server.js +++ b/server/app/utils/plus-server.js @@ -1,8 +1,9 @@ module.exports = { plusServers: [ + 'https://en1.221022.xyz', + 'https://en2.221022.xyz', 'https://en1-plus-active-tencent-eo.chaoszhu.com', 'https://en2-plus-active-tencent-eo.chaoszhu.com', - 'https://en1.221022.xyz', 'https://en2.221022.xyz' ] } diff --git a/server/app/utils/plus.js b/server/app/utils/plus.js index 4341cc8..067eb9a 100644 --- a/server/app/utils/plus.js +++ b/server/app/utils/plus.js @@ -1 +1 @@ -U2FsdGVkX19ypJFyeFxNF22xekv1FOR0XT1ofsw1MQ8fs/xIpP4rByqxDo2e7/E4w3sSaVpT3IUt5jH0alt4GeryiM3wfedmo+4eBiw2qTTBW0tQHCbh2tIIxJWPv3NdaMCv0fEzJNJf2CrhgrRSv9RpUpDYYojpg50wnKjL2Fiw37mGw9uqHowmgqoyxO14Otjbh9Sf2Y9DqoYr4SF/uvxPGeFpBO3op/+8gnqgN3r0mmF8DLn73m5a7NANNe6IuHXCwfu3DY9q9xgI6OEp2ecW/rxXijGRllMmVkdSKeG0xzSeV7q1PF+qpvEo9xz8FdVBOW4Te6G/hDpwA3WI0cwKY5nOs+aY4J8KNmtpsQnUMbjm9w5OjCrKOBzyQ6JIYbT6b7vP4jyF6FLOR0DJJ3JRP1OGbDLZhoWkXtNB7Mwa85KegKVx3Hg2eEsEug93jQrymm++YtFvoEbdAD5AeA8NX/qJRVqloOikv4d/mCy1BJaAQE4tAHNIldwMCvTarUKrYSxjdzO0sZmRuE4PhwjIGIN5dK8/GYR9vFfnGuNhM+w0gRJr1VDK+/6XnMp0PmQMx7MYO4UE2A4zulIsxhWxYRhzvmePPWf5rOIPnSl8dYx/otqgs6LY+FDkMWa99itdFFhCZxjapUJ2917ofYuAH8Y76QliRVgZ9HOPgpJOr/A02dt3c+Kp/p9cu6vAuGJ29l9JDEr1d35+/hRvvR6SEWYoalFEjkyEl4Y1cuxyNasAxkpxFlN9/ROl1D3qDk1L/lci4YC1aXlyW8jLyNthIXFMHWTiawA7xEV0iFiSMjDkpIpv+1/V8T+b7djl13/ALfgw/Zw7rNDKNqj8vaYLNO2T7epyFeESWE8EY9Q= \ No newline at end of file +U2FsdGVkX19cLtuM32n63Jkv0/ObLTjPu6qgg7GgYXu5azvBJiwxIKAZpdoIwwJyxqsRh5oggNeUZxVtLyQ2DPvBV8N+e4P9SCIZ4AwL0NoQzgwes1gjKAR6bfVxCJ1KjB2oSa+HQLtKc9grodpsF++KLNC8+D1ZQBWVQ7Florqr0Zn0Nq2v3tsO2wS22QEGnmtTQaGELMnBgNti1EuBk7XAcChOCIY3KSsELowIdcFyyAlouc/YJiholQELTc3R3li3GSlmOuFPTnnaLe/PBMxjbTApma2otVTl5ddNkKzMrMSyOhBprw1nOVK2k/sHhCcZtoVBp3iT/13Dv50W2YTAw8k9Tb52w1jcn+fDIcj1EBZNOCTUhJ7TJXgncFCvNuRWKwFrx3vHJWW3Qaep/PMRbv+UYfWnU56kXBldOMTeyvIK4GRrm109fSBAf3/Yrt1MquIkt6GLN5x3eF9p/CwSF5ObykkVa4A7mI10wRcY9Le/HyZbcbdkyICA8Y9+ZM9PAOP1rO7W268NJzc//y+tBH9Lh9X7beK6xR0soMkGsboAsGCSQGG8kLSlCtYSN2dWp2GqNrtgrMjqX2STri+/2PTdJ3rQFoABlIl24qLoZsgcsfiEm3MGnTcSCl2sbnJM0MSCdlTCrozmm9h7yFzrdKwxQamtzGd9UYzFzBUqGMmdXj0MxG0Wt8rGJ+ALM5NWJPV9JdXzsVGTMJcuUUole/ZUAnoeOyVPZGw9u2zve1G6DqxTlt7lTdqt+r7YQs8xVqKzNWhrs4JaHN8VC5vpCDAzPxukPfd7spfLqWn97llTP5zR/b4DiM5vwTuWgHgeXp4s3Ii2hgJW7yim8cWk7Q5phP1mvb0PorlSp+4= \ No newline at end of file diff --git a/server/app/utils/tools.js b/server/app/utils/tools.js index 0dc2d1f..1f09a05 100644 --- a/server/app/utils/tools.js +++ b/server/app/utils/tools.js @@ -259,6 +259,17 @@ let shellThrottle = (fn, delay = 1000) => { return throttled } +const fileTransferThrottle = (fn, delay = 1500) => { + let lastCall = 0 + return function (...args) { + const now = Date.now() + if (now - lastCall >= delay) { + lastCall = now + fn(...args) + } + } +} + const isProd = () => { const EXEC_ENV = process.env.EXEC_ENV || 'production' return EXEC_ENV === 'production' @@ -364,6 +375,7 @@ module.exports = { formatTimestamp, resolvePath, shellThrottle, + fileTransferThrottle, isProd, isAllowedIp, ping, diff --git a/server/package.json b/server/package.json index 66e55d9..285e5cb 100644 --- a/server/package.json +++ b/server/package.json @@ -1,6 +1,6 @@ { "name": "server", - "version": "3.3.0", + "version": "3.4.0", "description": "easynode-server", "bin": "./bin/www", "scripts": { diff --git a/web/package.json b/web/package.json index 96dbaa1..a634185 100644 --- a/web/package.json +++ b/web/package.json @@ -1,6 +1,6 @@ { "name": "web", - "version": "3.3.0", + "version": "3.4.0", "description": "easynode-web", "private": true, "scripts": { diff --git a/web/src/assets/scss/element/index.scss b/web/src/assets/scss/element/index.scss index ea1b2a4..5f4a706 100644 --- a/web/src/assets/scss/element/index.scss +++ b/web/src/assets/scss/element/index.scss @@ -11,4 +11,8 @@ .el-collapse { border-bottom-width: 0px; +} + +.el-loading-mask { + z-index: 99!important; } \ No newline at end of file diff --git a/web/src/components/file-transfer/sftp-panel.vue b/web/src/components/file-transfer/sftp-panel.vue new file mode 100644 index 0000000..2140e93 --- /dev/null +++ b/web/src/components/file-transfer/sftp-panel.vue @@ -0,0 +1,260 @@ + + + + + \ No newline at end of file diff --git a/web/src/components/file-transfer/transfer-preview.vue b/web/src/components/file-transfer/transfer-preview.vue new file mode 100644 index 0000000..95204f8 --- /dev/null +++ b/web/src/components/file-transfer/transfer-preview.vue @@ -0,0 +1,358 @@ + + + + + \ No newline at end of file diff --git a/web/src/components/file-transfer/transfer-task-manager.vue b/web/src/components/file-transfer/transfer-task-manager.vue new file mode 100644 index 0000000..2b5d7bf --- /dev/null +++ b/web/src/components/file-transfer/transfer-task-manager.vue @@ -0,0 +1,650 @@ + + + + + \ No newline at end of file diff --git a/web/src/components/plus-table.vue b/web/src/components/plus-table.vue index eb86d7a..f347974 100644 --- a/web/src/components/plus-table.vue +++ b/web/src/components/plus-table.vue @@ -95,6 +95,7 @@ const basicFeatures = [ const plusFeatures = [ 'AI Chat对话组件', '服务器代理&跳板机功能', + '文件对传', '终端单窗口模式', '批量修改实例配置', '脚本库批量导出导入', @@ -102,7 +103,6 @@ const plusFeatures = [ '终端功能栏docker容器管理', '凭据管理支持解密带密码保护的密钥', '通知方式无限制', - '本地socket断开自动重连', ] diff --git a/web/src/components/server-selector.vue b/web/src/components/server-selector.vue new file mode 100644 index 0000000..3051cda --- /dev/null +++ b/web/src/components/server-selector.vue @@ -0,0 +1,77 @@ + + + + + \ No newline at end of file diff --git a/web/src/views/file/index.vue b/web/src/views/file/index.vue index e27e437..4da9729 100644 --- a/web/src/views/file/index.vue +++ b/web/src/views/file/index.vue @@ -1,25 +1,500 @@ diff --git a/web/src/views/terminal/components/sftp-v2.vue b/web/src/views/terminal/components/sftp-v2.vue index a97cee3..5ecae9d 100644 --- a/web/src/views/terminal/components/sftp-v2.vue +++ b/web/src/views/terminal/components/sftp-v2.vue @@ -13,7 +13,7 @@
-

SFTP连接失败

+

SFTP连接断开

{{ connectionError || '请检查服务端状态或网络连接' }}

重新连接
@@ -81,6 +81,11 @@ + + + + +
@@ -532,7 +537,7 @@