feat(remote): 完善文件传输功能及WebSocket支持
This commit is contained in:
@@ -262,6 +262,124 @@ ipcMain.handle("clipboard-write-text", async (event, text) => {
|
|||||||
return { success: false, error: error.message };
|
return { success: false, error: error.message };
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
ipcMain.handle("remote-fetch-drives", async (_event, serverHost, port, password) => {
|
||||||
|
try {
|
||||||
|
let url = `http://${serverHost}:${port}/api/files/drives`;
|
||||||
|
if (password) {
|
||||||
|
url += `?password=${encodeURIComponent(password)}`;
|
||||||
|
}
|
||||||
|
const response = await fetch(url);
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`Failed to fetch drives: ${response.statusText}`);
|
||||||
|
}
|
||||||
|
const data = await response.json();
|
||||||
|
const items = data.items || [];
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
data: items.map((item) => ({
|
||||||
|
name: item.name,
|
||||||
|
path: item.name,
|
||||||
|
type: item.isDirectory ? "dir" : "file",
|
||||||
|
size: item.size,
|
||||||
|
modified: ""
|
||||||
|
}))
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
log2.error("Remote fetch drives failed:", error);
|
||||||
|
return { success: false, error: error.message };
|
||||||
|
}
|
||||||
|
});
|
||||||
|
ipcMain.handle("remote-fetch-files", async (_event, serverHost, port, filePath, password) => {
|
||||||
|
try {
|
||||||
|
let url = `http://${serverHost}:${port}/api/files/browse?path=${encodeURIComponent(filePath)}&allowSystem=true`;
|
||||||
|
if (password) {
|
||||||
|
url += `&password=${encodeURIComponent(password)}`;
|
||||||
|
}
|
||||||
|
const response = await fetch(url);
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`Failed to fetch files: ${response.statusText}`);
|
||||||
|
}
|
||||||
|
const data = await response.json();
|
||||||
|
const items = data.items || [];
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
data: items.map((item) => ({
|
||||||
|
name: item.name,
|
||||||
|
path: data.currentPath ? `${data.currentPath}/${item.name}` : item.name,
|
||||||
|
type: item.isDirectory ? "dir" : "file",
|
||||||
|
size: item.size,
|
||||||
|
modified: item.modified?.toString()
|
||||||
|
}))
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
log2.error("Remote fetch files failed:", error);
|
||||||
|
return { success: false, error: error.message };
|
||||||
|
}
|
||||||
|
});
|
||||||
|
ipcMain.handle("remote-upload-file", async (_event, serverHost, port, filePath, remotePath, password) => {
|
||||||
|
try {
|
||||||
|
const win = electronState.getMainWindow();
|
||||||
|
if (!win) {
|
||||||
|
throw new Error("No window found");
|
||||||
|
}
|
||||||
|
const fullPath = path2.resolve(filePath);
|
||||||
|
if (!fs2.existsSync(fullPath)) {
|
||||||
|
throw new Error("File not found");
|
||||||
|
}
|
||||||
|
const fileBuffer = fs2.readFileSync(fullPath);
|
||||||
|
const fileName = path2.basename(fullPath);
|
||||||
|
let url = `http://${serverHost}:${port}/api/files/upload`;
|
||||||
|
if (password) {
|
||||||
|
url += `?password=${encodeURIComponent(password)}`;
|
||||||
|
}
|
||||||
|
const formData = new FormData();
|
||||||
|
const blob = new Blob([fileBuffer]);
|
||||||
|
formData.append("file", blob, fileName);
|
||||||
|
if (remotePath) {
|
||||||
|
formData.append("path", remotePath);
|
||||||
|
}
|
||||||
|
const response = await fetch(url, {
|
||||||
|
method: "POST",
|
||||||
|
body: formData
|
||||||
|
});
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`Upload failed: ${response.statusText}`);
|
||||||
|
}
|
||||||
|
return { success: true };
|
||||||
|
} catch (error) {
|
||||||
|
log2.error("Remote upload failed:", error);
|
||||||
|
return { success: false, error: error.message };
|
||||||
|
}
|
||||||
|
});
|
||||||
|
ipcMain.handle("remote-download-file", async (_event, serverHost, port, fileName, remotePath, password) => {
|
||||||
|
try {
|
||||||
|
const win = electronState.getMainWindow();
|
||||||
|
if (!win) {
|
||||||
|
throw new Error("No window found");
|
||||||
|
}
|
||||||
|
let url = `http://${serverHost}:${port}/api/files/${encodeURIComponent(fileName)}`;
|
||||||
|
if (password) {
|
||||||
|
url += `?password=${encodeURIComponent(password)}`;
|
||||||
|
}
|
||||||
|
const response = await fetch(url);
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`Download failed: ${response.statusText}`);
|
||||||
|
}
|
||||||
|
const buffer = await response.arrayBuffer();
|
||||||
|
const { filePath } = await dialog2.showSaveDialog(win, {
|
||||||
|
title: "\u4FDD\u5B58\u6587\u4EF6",
|
||||||
|
defaultPath: fileName
|
||||||
|
});
|
||||||
|
if (!filePath) {
|
||||||
|
return { success: false, canceled: true };
|
||||||
|
}
|
||||||
|
fs2.writeFileSync(filePath, Buffer.from(buffer));
|
||||||
|
return { success: true, filePath };
|
||||||
|
} catch (error) {
|
||||||
|
log2.error("Remote download failed:", error);
|
||||||
|
return { success: false, error: error.message };
|
||||||
|
}
|
||||||
|
});
|
||||||
async function startServer() {
|
async function startServer() {
|
||||||
if (electronState.isDevelopment()) {
|
if (electronState.isDevelopment()) {
|
||||||
log2.info("In dev mode, assuming external servers are running.");
|
log2.info("In dev mode, assuming external servers are running.");
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
@@ -19,6 +19,10 @@ import_electron.contextBridge.exposeInMainWorld("electronAPI", {
|
|||||||
return () => import_electron.ipcRenderer.removeListener("remote-clipboard-auto-sync", handler);
|
return () => import_electron.ipcRenderer.removeListener("remote-clipboard-auto-sync", handler);
|
||||||
},
|
},
|
||||||
clipboardReadText: () => import_electron.ipcRenderer.invoke("clipboard-read-text"),
|
clipboardReadText: () => import_electron.ipcRenderer.invoke("clipboard-read-text"),
|
||||||
clipboardWriteText: (text) => import_electron.ipcRenderer.invoke("clipboard-write-text", text)
|
clipboardWriteText: (text) => import_electron.ipcRenderer.invoke("clipboard-write-text", text),
|
||||||
|
remoteFetchDrives: (serverHost, port, password) => import_electron.ipcRenderer.invoke("remote-fetch-drives", serverHost, port, password),
|
||||||
|
remoteFetchFiles: (serverHost, port, filePath, password) => import_electron.ipcRenderer.invoke("remote-fetch-files", serverHost, port, filePath, password),
|
||||||
|
remoteUploadFile: (serverHost, port, filePath, remotePath, password) => import_electron.ipcRenderer.invoke("remote-upload-file", serverHost, port, filePath, remotePath, password),
|
||||||
|
remoteDownloadFile: (serverHost, port, fileName, remotePath, password) => import_electron.ipcRenderer.invoke("remote-download-file", serverHost, port, fileName, remotePath, password)
|
||||||
});
|
});
|
||||||
//# sourceMappingURL=preload.cjs.map
|
//# sourceMappingURL=preload.cjs.map
|
||||||
@@ -1 +1 @@
|
|||||||
{"version":3,"sources":["../electron/preload.ts"],"sourcesContent":["import { contextBridge, ipcRenderer } from 'electron'\n\nconsole.log('--- PRELOAD SCRIPT LOADED SUCCESSFULLY ---')\n\ncontextBridge.exposeInMainWorld('electronAPI', {\n exportPDF: (title: string, htmlContent?: string) => ipcRenderer.invoke('export-pdf', title, htmlContent),\n selectHtmlFile: () => ipcRenderer.invoke('select-html-file'),\n updateTitlebarButtons: (symbolColor: string) => ipcRenderer.invoke('update-titlebar-buttons', symbolColor),\n onRemoteClipboardSyncToRemote: (callback: () => void) => {\n ipcRenderer.on('remote-clipboard-sync-to-remote', callback);\n return () => ipcRenderer.removeListener('remote-clipboard-sync-to-remote', callback);\n },\n onRemoteClipboardSyncFromRemote: (callback: () => void) => {\n ipcRenderer.on('remote-clipboard-sync-from-remote', callback);\n return () => ipcRenderer.removeListener('remote-clipboard-sync-from-remote', callback);\n },\n onRemoteClipboardAutoSync: (callback: (text: string) => void) => {\n const handler = (_event: Electron.IpcRendererEvent, text: string) => callback(text);\n ipcRenderer.on('remote-clipboard-auto-sync', handler);\n return () => ipcRenderer.removeListener('remote-clipboard-auto-sync', handler);\n },\n clipboardReadText: () => ipcRenderer.invoke('clipboard-read-text'),\n clipboardWriteText: (text: string) => ipcRenderer.invoke('clipboard-write-text', text),\n})\n"],"mappings":";AAAA,sBAA2C;AAE3C,QAAQ,IAAI,4CAA4C;AAExD,8BAAc,kBAAkB,eAAe;AAAA,EAC7C,WAAW,CAAC,OAAe,gBAAyB,4BAAY,OAAO,cAAc,OAAO,WAAW;AAAA,EACvG,gBAAgB,MAAM,4BAAY,OAAO,kBAAkB;AAAA,EAC3D,uBAAuB,CAAC,gBAAwB,4BAAY,OAAO,2BAA2B,WAAW;AAAA,EACzG,+BAA+B,CAAC,aAAyB;AACvD,gCAAY,GAAG,mCAAmC,QAAQ;AAC1D,WAAO,MAAM,4BAAY,eAAe,mCAAmC,QAAQ;AAAA,EACrF;AAAA,EACA,iCAAiC,CAAC,aAAyB;AACzD,gCAAY,GAAG,qCAAqC,QAAQ;AAC5D,WAAO,MAAM,4BAAY,eAAe,qCAAqC,QAAQ;AAAA,EACvF;AAAA,EACA,2BAA2B,CAAC,aAAqC;AAC/D,UAAM,UAAU,CAAC,QAAmC,SAAiB,SAAS,IAAI;AAClF,gCAAY,GAAG,8BAA8B,OAAO;AACpD,WAAO,MAAM,4BAAY,eAAe,8BAA8B,OAAO;AAAA,EAC/E;AAAA,EACA,mBAAmB,MAAM,4BAAY,OAAO,qBAAqB;AAAA,EACjE,oBAAoB,CAAC,SAAiB,4BAAY,OAAO,wBAAwB,IAAI;AACvF,CAAC;","names":[]}
|
{"version":3,"sources":["../electron/preload.ts"],"sourcesContent":["import { contextBridge, ipcRenderer } from 'electron'\r\n\r\nconsole.log('--- PRELOAD SCRIPT LOADED SUCCESSFULLY ---')\r\n\r\ncontextBridge.exposeInMainWorld('electronAPI', {\r\n exportPDF: (title: string, htmlContent?: string) => ipcRenderer.invoke('export-pdf', title, htmlContent),\r\n selectHtmlFile: () => ipcRenderer.invoke('select-html-file'),\r\n updateTitlebarButtons: (symbolColor: string) => ipcRenderer.invoke('update-titlebar-buttons', symbolColor),\r\n onRemoteClipboardSyncToRemote: (callback: () => void) => {\r\n ipcRenderer.on('remote-clipboard-sync-to-remote', callback);\r\n return () => ipcRenderer.removeListener('remote-clipboard-sync-to-remote', callback);\r\n },\r\n onRemoteClipboardSyncFromRemote: (callback: () => void) => {\r\n ipcRenderer.on('remote-clipboard-sync-from-remote', callback);\r\n return () => ipcRenderer.removeListener('remote-clipboard-sync-from-remote', callback);\r\n },\r\n onRemoteClipboardAutoSync: (callback: (text: string) => void) => {\r\n const handler = (_event: Electron.IpcRendererEvent, text: string) => callback(text);\r\n ipcRenderer.on('remote-clipboard-auto-sync', handler);\r\n return () => ipcRenderer.removeListener('remote-clipboard-auto-sync', handler);\r\n },\r\n clipboardReadText: () => ipcRenderer.invoke('clipboard-read-text'),\r\n clipboardWriteText: (text: string) => ipcRenderer.invoke('clipboard-write-text', text),\r\n remoteFetchDrives: (serverHost: string, port: number, password?: string) => \r\n ipcRenderer.invoke('remote-fetch-drives', serverHost, port, password),\r\n remoteFetchFiles: (serverHost: string, port: number, filePath: string, password?: string) => \r\n ipcRenderer.invoke('remote-fetch-files', serverHost, port, filePath, password),\r\n remoteUploadFile: (serverHost: string, port: number, filePath: string, remotePath: string, password?: string) => \r\n ipcRenderer.invoke('remote-upload-file', serverHost, port, filePath, remotePath, password),\r\n remoteDownloadFile: (serverHost: string, port: number, fileName: string, remotePath: string, password?: string) => \r\n ipcRenderer.invoke('remote-download-file', serverHost, port, fileName, remotePath, password),\r\n})\r\n"],"mappings":";AAAA,sBAA2C;AAE3C,QAAQ,IAAI,4CAA4C;AAExD,8BAAc,kBAAkB,eAAe;AAAA,EAC7C,WAAW,CAAC,OAAe,gBAAyB,4BAAY,OAAO,cAAc,OAAO,WAAW;AAAA,EACvG,gBAAgB,MAAM,4BAAY,OAAO,kBAAkB;AAAA,EAC3D,uBAAuB,CAAC,gBAAwB,4BAAY,OAAO,2BAA2B,WAAW;AAAA,EACzG,+BAA+B,CAAC,aAAyB;AACvD,gCAAY,GAAG,mCAAmC,QAAQ;AAC1D,WAAO,MAAM,4BAAY,eAAe,mCAAmC,QAAQ;AAAA,EACrF;AAAA,EACA,iCAAiC,CAAC,aAAyB;AACzD,gCAAY,GAAG,qCAAqC,QAAQ;AAC5D,WAAO,MAAM,4BAAY,eAAe,qCAAqC,QAAQ;AAAA,EACvF;AAAA,EACA,2BAA2B,CAAC,aAAqC;AAC/D,UAAM,UAAU,CAAC,QAAmC,SAAiB,SAAS,IAAI;AAClF,gCAAY,GAAG,8BAA8B,OAAO;AACpD,WAAO,MAAM,4BAAY,eAAe,8BAA8B,OAAO;AAAA,EAC/E;AAAA,EACA,mBAAmB,MAAM,4BAAY,OAAO,qBAAqB;AAAA,EACjE,oBAAoB,CAAC,SAAiB,4BAAY,OAAO,wBAAwB,IAAI;AAAA,EACrF,mBAAmB,CAAC,YAAoB,MAAc,aACpD,4BAAY,OAAO,uBAAuB,YAAY,MAAM,QAAQ;AAAA,EACtE,kBAAkB,CAAC,YAAoB,MAAc,UAAkB,aACrE,4BAAY,OAAO,sBAAsB,YAAY,MAAM,UAAU,QAAQ;AAAA,EAC/E,kBAAkB,CAAC,YAAoB,MAAc,UAAkB,YAAoB,aACzF,4BAAY,OAAO,sBAAsB,YAAY,MAAM,UAAU,YAAY,QAAQ;AAAA,EAC3F,oBAAoB,CAAC,YAAoB,MAAc,UAAkB,YAAoB,aAC3F,4BAAY,OAAO,wBAAwB,YAAY,MAAM,UAAU,YAAY,QAAQ;AAC/F,CAAC;","names":[]}
|
||||||
0
remote.err
Normal file
0
remote.err
Normal file
@@ -27,4 +27,11 @@ localIP = "127.0.0.1"
|
|||||||
localPort = 3002
|
localPort = 3002
|
||||||
remotePort = 8082
|
remotePort = 8082
|
||||||
|
|
||||||
|
[[proxies]]
|
||||||
|
name = "filetransfer-remote"
|
||||||
|
type = "tcp"
|
||||||
|
localIP = "127.0.0.1"
|
||||||
|
localPort = 3003
|
||||||
|
remotePort = 8083
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -68,6 +68,17 @@ router.get('/browse', (req, res) => {
|
|||||||
try {
|
try {
|
||||||
const filePath = req.query.path || '';
|
const filePath = req.query.path || '';
|
||||||
const allowSystem = req.query.allowSystem === 'true';
|
const allowSystem = req.query.allowSystem === 'true';
|
||||||
|
|
||||||
|
if (allowSystem && !filePath) {
|
||||||
|
const drives = fileService.getDrives();
|
||||||
|
res.json({
|
||||||
|
items: drives,
|
||||||
|
currentPath: '',
|
||||||
|
parentPath: null
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const result = fileService.browseDirectory(filePath, allowSystem);
|
const result = fileService.browseDirectory(filePath, allowSystem);
|
||||||
res.json(result);
|
res.json(result);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|||||||
261
remote/src/server/FileHandler.js
Normal file
261
remote/src/server/FileHandler.js
Normal file
@@ -0,0 +1,261 @@
|
|||||||
|
const crypto = require('crypto');
|
||||||
|
const fs = require('fs');
|
||||||
|
const path = require('path');
|
||||||
|
const logger = require('../utils/logger');
|
||||||
|
const MessageTypes = require('./messageTypes');
|
||||||
|
const { fileService } = require('../services/file');
|
||||||
|
|
||||||
|
const CHUNK_SIZE = 5 * 1024 * 1024;
|
||||||
|
|
||||||
|
class FileHandler {
|
||||||
|
constructor() {
|
||||||
|
this.uploadSessions = new Map();
|
||||||
|
}
|
||||||
|
|
||||||
|
handleMessage(message, ws) {
|
||||||
|
const { type, requestId, ...data } = message;
|
||||||
|
|
||||||
|
switch (type) {
|
||||||
|
case MessageTypes.FILE_LIST_GET:
|
||||||
|
this.handleFileList(ws, requestId);
|
||||||
|
break;
|
||||||
|
case MessageTypes.FILE_BROWSE:
|
||||||
|
this.handleFileBrowse(ws, data, requestId);
|
||||||
|
break;
|
||||||
|
case MessageTypes.FILE_UPLOAD_START:
|
||||||
|
this.handleFileUploadStart(ws, data, requestId);
|
||||||
|
break;
|
||||||
|
case MessageTypes.FILE_UPLOAD_CHUNK:
|
||||||
|
this.handleFileUploadChunk(ws, data, requestId);
|
||||||
|
break;
|
||||||
|
case MessageTypes.FILE_UPLOAD_MERGE:
|
||||||
|
this.handleFileUploadMerge(ws, data, requestId);
|
||||||
|
break;
|
||||||
|
case MessageTypes.FILE_DOWNLOAD_START:
|
||||||
|
this.handleFileDownload(ws, data, requestId);
|
||||||
|
break;
|
||||||
|
case MessageTypes.FILE_DELETE:
|
||||||
|
this.handleFileDelete(ws, data, requestId);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
logger.debug('Unknown file message type', { type });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
sendResponse(ws, type, requestId, payload) {
|
||||||
|
ws.send(JSON.stringify({
|
||||||
|
type,
|
||||||
|
requestId,
|
||||||
|
...payload
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
handleFileList(ws, requestId) {
|
||||||
|
try {
|
||||||
|
const files = fileService.getFileList();
|
||||||
|
this.sendResponse(ws, MessageTypes.FILE_LIST, requestId, { files });
|
||||||
|
} catch (error) {
|
||||||
|
logger.error('Failed to get file list', { error: error.message });
|
||||||
|
this.sendResponse(ws, MessageTypes.FILE_LIST, requestId, { files: [], error: error.message });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
handleFileBrowse(ws, data, requestId) {
|
||||||
|
try {
|
||||||
|
const { path: dirPath, allowSystem } = data;
|
||||||
|
|
||||||
|
if (allowSystem && !dirPath) {
|
||||||
|
const drives = fileService.getDrives();
|
||||||
|
this.sendResponse(ws, MessageTypes.FILE_BROWSE_RESULT, requestId, {
|
||||||
|
items: drives,
|
||||||
|
currentPath: '',
|
||||||
|
parentPath: null
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = fileService.browseDirectory(dirPath || '', allowSystem === true);
|
||||||
|
this.sendResponse(ws, MessageTypes.FILE_BROWSE_RESULT, requestId, result);
|
||||||
|
} catch (error) {
|
||||||
|
logger.error('Failed to browse directory', { error: error.message });
|
||||||
|
this.sendResponse(ws, MessageTypes.FILE_BROWSE_RESULT, requestId, { items: [], error: error.message });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
handleFileUploadStart(ws, data, requestId) {
|
||||||
|
try {
|
||||||
|
const { filename, totalChunks, fileSize } = data;
|
||||||
|
const fileId = data.fileId || requestId || crypto.randomBytes(16).toString('hex');
|
||||||
|
|
||||||
|
this.uploadSessions.set(fileId, {
|
||||||
|
filename,
|
||||||
|
totalChunks,
|
||||||
|
fileSize,
|
||||||
|
chunks: new Map(),
|
||||||
|
createdAt: Date.now()
|
||||||
|
});
|
||||||
|
|
||||||
|
this.sendResponse(ws, MessageTypes.FILE_UPLOAD_START, requestId, {
|
||||||
|
fileId,
|
||||||
|
chunkSize: CHUNK_SIZE,
|
||||||
|
message: 'Upload session started'
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
logger.error('Failed to start upload', { error: error.message });
|
||||||
|
this.sendResponse(ws, MessageTypes.FILE_UPLOAD_RESULT, requestId, { success: false, error: error.message });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
handleFileUploadChunk(ws, data, requestId) {
|
||||||
|
try {
|
||||||
|
const { fileId, chunkIndex } = data;
|
||||||
|
const session = this.uploadSessions.get(fileId);
|
||||||
|
|
||||||
|
if (!session) {
|
||||||
|
this.sendResponse(ws, MessageTypes.FILE_UPLOAD_RESULT, requestId, { success: false, error: 'Upload session not found' });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let chunkData;
|
||||||
|
if (data.data) {
|
||||||
|
chunkData = Buffer.from(data.data, 'base64');
|
||||||
|
} else if (data.buffer) {
|
||||||
|
chunkData = Buffer.from(data.buffer);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!chunkData) {
|
||||||
|
this.sendResponse(ws, MessageTypes.FILE_UPLOAD_RESULT, requestId, { success: false, error: 'No chunk data provided' });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
session.chunks.set(chunkIndex, chunkData);
|
||||||
|
|
||||||
|
this.sendResponse(ws, MessageTypes.FILE_UPLOAD_CHUNK, requestId, { success: true, chunkIndex });
|
||||||
|
} catch (error) {
|
||||||
|
logger.error('Failed to upload chunk', { error: error.message });
|
||||||
|
this.sendResponse(ws, MessageTypes.FILE_UPLOAD_RESULT, requestId, { success: false, error: error.message });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
handleFileUploadMerge(ws, data, requestId) {
|
||||||
|
try {
|
||||||
|
const { fileId, filename } = data;
|
||||||
|
const session = this.uploadSessions.get(fileId);
|
||||||
|
|
||||||
|
if (!session) {
|
||||||
|
this.sendResponse(ws, MessageTypes.FILE_UPLOAD_RESULT, requestId, { success: false, error: 'Upload session not found' });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const success = fileService.mergeChunks(fileId, session.totalChunks, filename);
|
||||||
|
|
||||||
|
this.uploadSessions.delete(fileId);
|
||||||
|
|
||||||
|
if (success) {
|
||||||
|
fileService.cleanupChunks(fileId);
|
||||||
|
this.sendResponse(ws, MessageTypes.FILE_UPLOAD_RESULT, requestId, { success: true, filename });
|
||||||
|
} else {
|
||||||
|
fileService.cleanupChunks(fileId);
|
||||||
|
this.sendResponse(ws, MessageTypes.FILE_UPLOAD_RESULT, requestId, { success: false, error: 'Failed to merge chunks' });
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
logger.error('Failed to merge chunks', { error: error.message });
|
||||||
|
this.sendResponse(ws, MessageTypes.FILE_UPLOAD_RESULT, requestId, { success: false, error: error.message });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
handleFileDownload(ws, data, requestId) {
|
||||||
|
try {
|
||||||
|
const { filename, filePath, allowSystem } = data;
|
||||||
|
|
||||||
|
let fullFilePath;
|
||||||
|
if (allowSystem && filePath && (path.isAbsolute(filePath) || filePath.includes(':') || filePath.startsWith('\\') || filePath.startsWith('/'))) {
|
||||||
|
if (path.isAbsolute(filePath)) {
|
||||||
|
fullFilePath = filePath;
|
||||||
|
} else {
|
||||||
|
fullFilePath = filePath.replace(/\//g, '\\');
|
||||||
|
}
|
||||||
|
} else if (filePath) {
|
||||||
|
fullFilePath = path.join(fileService.uploadDir, filePath);
|
||||||
|
} else {
|
||||||
|
fullFilePath = path.join(fileService.uploadDir, filename);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!fs.existsSync(fullFilePath)) {
|
||||||
|
this.sendResponse(ws, MessageTypes.FILE_DOWNLOAD_COMPLETE, requestId, { success: false, error: 'File not found: ' + fullFilePath });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const stat = fs.statSync(fullFilePath);
|
||||||
|
const fileSize = stat.size;
|
||||||
|
const totalChunks = Math.ceil(fileSize / CHUNK_SIZE);
|
||||||
|
|
||||||
|
this.sendResponse(ws, MessageTypes.FILE_DOWNLOAD_START, requestId, {
|
||||||
|
filename,
|
||||||
|
size: fileSize,
|
||||||
|
chunkSize: CHUNK_SIZE,
|
||||||
|
totalChunks
|
||||||
|
});
|
||||||
|
|
||||||
|
const stream = fs.createReadStream(fullFilePath);
|
||||||
|
let chunkIndex = 0;
|
||||||
|
|
||||||
|
stream.on('data', (chunk) => {
|
||||||
|
ws.send(JSON.stringify({
|
||||||
|
type: MessageTypes.FILE_DOWNLOAD_CHUNK,
|
||||||
|
chunkIndex,
|
||||||
|
data: chunk.toString('base64'),
|
||||||
|
progress: Math.round(((chunkIndex + 1) / totalChunks) * 100)
|
||||||
|
}));
|
||||||
|
|
||||||
|
chunkIndex++;
|
||||||
|
});
|
||||||
|
|
||||||
|
stream.on('end', () => {
|
||||||
|
ws.send(JSON.stringify({
|
||||||
|
type: MessageTypes.FILE_DOWNLOAD_COMPLETE,
|
||||||
|
success: true,
|
||||||
|
filename
|
||||||
|
}));
|
||||||
|
});
|
||||||
|
|
||||||
|
stream.on('error', (error) => {
|
||||||
|
logger.error('File download error', { error: error.message });
|
||||||
|
ws.send(JSON.stringify({
|
||||||
|
type: MessageTypes.FILE_DOWNLOAD_COMPLETE,
|
||||||
|
success: false,
|
||||||
|
error: error.message
|
||||||
|
}));
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
logger.error('Failed to start download', { error: error.message });
|
||||||
|
this.sendResponse(ws, MessageTypes.FILE_DOWNLOAD_COMPLETE, requestId, { success: false, error: error.message });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
handleFileDelete(ws, data, requestId) {
|
||||||
|
try {
|
||||||
|
const { filename, filePath } = data;
|
||||||
|
const targetPath = filePath || '';
|
||||||
|
const success = fileService.deleteFile(filename, targetPath);
|
||||||
|
|
||||||
|
this.sendResponse(ws, MessageTypes.FILE_DELETE_RESULT, requestId, { success, filename });
|
||||||
|
} catch (error) {
|
||||||
|
logger.error('Failed to delete file', { error: error.message });
|
||||||
|
this.sendResponse(ws, MessageTypes.FILE_DELETE_RESULT, requestId, { success: false, error: error.message });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
cleanup() {
|
||||||
|
const now = Date.now();
|
||||||
|
const maxAge = 30 * 60 * 1000;
|
||||||
|
|
||||||
|
for (const [fileId, session] of this.uploadSessions) {
|
||||||
|
if (now - session.createdAt > maxAge) {
|
||||||
|
this.uploadSessions.delete(fileId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = FileHandler;
|
||||||
78
remote/src/server/FileServer.js
Normal file
78
remote/src/server/FileServer.js
Normal file
@@ -0,0 +1,78 @@
|
|||||||
|
const express = require('express');
|
||||||
|
const http = require('http');
|
||||||
|
const WebSocket = require('ws');
|
||||||
|
const logger = require('../utils/logger');
|
||||||
|
const FileHandler = require('./FileHandler');
|
||||||
|
|
||||||
|
class FileServer {
|
||||||
|
constructor(port = 3001) {
|
||||||
|
this.port = port;
|
||||||
|
this.app = express();
|
||||||
|
this.server = http.createServer(this.app);
|
||||||
|
this.wss = null;
|
||||||
|
this.fileHandler = new FileHandler();
|
||||||
|
}
|
||||||
|
|
||||||
|
start() {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
this.server.listen({ port: this.port, host: '0.0.0.0' }, () => {
|
||||||
|
logger.info('File server started', { port: this.port });
|
||||||
|
this._setupWebSocket();
|
||||||
|
resolve({ port: this.port });
|
||||||
|
});
|
||||||
|
this.server.on('error', reject);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
_setupWebSocket() {
|
||||||
|
this.wss = new WebSocket.Server({ server: this.server, path: '/ws' });
|
||||||
|
|
||||||
|
this.wss.on('connection', (ws, req) => {
|
||||||
|
logger.info('File client connected', { ip: req.socket.remoteAddress });
|
||||||
|
|
||||||
|
ws.on('message', (data, isBinary) => {
|
||||||
|
try {
|
||||||
|
if (isBinary) {
|
||||||
|
logger.warn('Received binary data on file WebSocket, ignoring');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const dataStr = data.toString();
|
||||||
|
logger.info('Raw message received:', dataStr.substring(0, 300));
|
||||||
|
const message = JSON.parse(dataStr);
|
||||||
|
logger.info('File message parsed', { type: message.type, requestId: message.requestId });
|
||||||
|
this.fileHandler.handleMessage(message, ws);
|
||||||
|
} catch (error) {
|
||||||
|
logger.error('Failed to parse file message', { error: error.message });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
ws.on('close', () => {
|
||||||
|
logger.info('File client disconnected');
|
||||||
|
});
|
||||||
|
|
||||||
|
ws.on('error', (error) => {
|
||||||
|
logger.error('File WebSocket error', { error: error.message });
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
this.wss.on('error', (error) => {
|
||||||
|
logger.error('File WebSocket server error', { error: error.message });
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
stop() {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
if (this.wss) {
|
||||||
|
this.wss.clients.forEach(client => client.close());
|
||||||
|
this.wss.close();
|
||||||
|
}
|
||||||
|
this.server.close((err) => {
|
||||||
|
if (err) reject(err);
|
||||||
|
else resolve();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = FileServer;
|
||||||
@@ -44,7 +44,8 @@ class FileService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
getFilePath(filename) {
|
getFilePath(filename) {
|
||||||
const filePath = path.join(this.uploadDir, path.basename(filename));
|
if (!filename) return null;
|
||||||
|
const filePath = path.normalize(filename);
|
||||||
if (!fs.existsSync(filePath)) {
|
if (!fs.existsSync(filePath)) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@@ -92,7 +93,13 @@ class FileService {
|
|||||||
|
|
||||||
mergeChunks(fileId, totalChunks, filename) {
|
mergeChunks(fileId, totalChunks, filename) {
|
||||||
try {
|
try {
|
||||||
const filePath = path.join(this.uploadDir, path.basename(filename));
|
const filePath = path.normalize(filename);
|
||||||
|
const dir = path.dirname(filePath);
|
||||||
|
|
||||||
|
if (!fs.existsSync(dir)) {
|
||||||
|
fs.mkdirSync(dir, { recursive: true });
|
||||||
|
}
|
||||||
|
|
||||||
const fd = fs.openSync(filePath, 'w');
|
const fd = fs.openSync(filePath, 'w');
|
||||||
|
|
||||||
for (let i = 0; i < totalChunks; i++) {
|
for (let i = 0; i < totalChunks; i++) {
|
||||||
|
|||||||
22
remote/test-connect.js
Normal file
22
remote/test-connect.js
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
const WebSocket = require('ws');
|
||||||
|
|
||||||
|
console.log('Testing connection to remote...');
|
||||||
|
const ws = new WebSocket('ws://146.56.248.142:8083/ws?password=wzw20040525', {
|
||||||
|
handshakeTimeout: 5000
|
||||||
|
});
|
||||||
|
|
||||||
|
ws.on('open', () => {
|
||||||
|
console.log('Connected!');
|
||||||
|
ws.close();
|
||||||
|
process.exit(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
ws.on('error', (err) => {
|
||||||
|
console.log('Error:', err.message);
|
||||||
|
process.exit(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
console.log('Connection timeout');
|
||||||
|
process.exit(1);
|
||||||
|
}, 8000);
|
||||||
268
remote/test-file-api.js
Normal file
268
remote/test-file-api.js
Normal file
@@ -0,0 +1,268 @@
|
|||||||
|
const http = require('http');
|
||||||
|
const https = require('https');
|
||||||
|
|
||||||
|
const BASE_URL = 'http://localhost:3000';
|
||||||
|
const PASSWORD = 'wzw20040525';
|
||||||
|
const CHUNK_SIZE = 5 * 1024 * 1024;
|
||||||
|
|
||||||
|
function request(options, body = null) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const url = new URL(options.path, BASE_URL);
|
||||||
|
url.searchParams.set('password', PASSWORD);
|
||||||
|
|
||||||
|
const client = url.protocol === 'https:' ? https : http;
|
||||||
|
|
||||||
|
const reqOptions = {
|
||||||
|
hostname: url.hostname,
|
||||||
|
port: url.port,
|
||||||
|
path: url.pathname + url.search,
|
||||||
|
method: options.method,
|
||||||
|
headers: options.headers || {}
|
||||||
|
};
|
||||||
|
|
||||||
|
const req = client.request(reqOptions, (res) => {
|
||||||
|
let data = '';
|
||||||
|
res.on('data', chunk => data += chunk);
|
||||||
|
res.on('end', () => {
|
||||||
|
try {
|
||||||
|
resolve({ status: res.statusCode, data: JSON.parse(data) });
|
||||||
|
} catch {
|
||||||
|
resolve({ status: res.statusCode, data });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
req.on('error', reject);
|
||||||
|
|
||||||
|
if (body) {
|
||||||
|
if (body instanceof FormData) {
|
||||||
|
req.write(body.getBuffer());
|
||||||
|
} else if (Buffer.isBuffer(body)) {
|
||||||
|
req.write(body);
|
||||||
|
} else {
|
||||||
|
req.write(JSON.stringify(body));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
req.end();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function testGetDrives() {
|
||||||
|
console.log('\n=== 测试1: 获取驱动器列表 ===');
|
||||||
|
try {
|
||||||
|
const driveUrl = new URL('/api/files/browse', BASE_URL);
|
||||||
|
driveUrl.searchParams.set('allowSystem', 'true');
|
||||||
|
driveUrl.searchParams.set('password', PASSWORD);
|
||||||
|
|
||||||
|
const res = await request({
|
||||||
|
method: 'GET',
|
||||||
|
path: driveUrl.pathname + driveUrl.search
|
||||||
|
});
|
||||||
|
console.log('状态:', res.status);
|
||||||
|
console.log('currentPath:', res.data.currentPath);
|
||||||
|
console.log('parentPath:', res.data.parentPath);
|
||||||
|
|
||||||
|
// 检查是否返回了驱动器(盘符如 C:, D:)
|
||||||
|
const drives = res.data.items?.filter(item => item.name.match(/^[A-Z]:$/i));
|
||||||
|
if (drives && drives.length > 0) {
|
||||||
|
console.log('✓ 驱动器列表:', drives.map(d => d.name).join(', '));
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果返回的是目录列表而非驱动器,说明 allowSystem 未生效
|
||||||
|
console.log('✗ 未获取到驱动器列表');
|
||||||
|
console.log('返回的项目:', res.data.items?.slice(0, 5).map(i => i.name).join(', '));
|
||||||
|
return false;
|
||||||
|
} catch (err) {
|
||||||
|
console.error('错误:', err.message);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function testUploadToSystemDir() {
|
||||||
|
console.log('\n=== 测试2: 上传到系统目录 ===');
|
||||||
|
const testContent = 'Hello Remote System Directory Test ' + Date.now();
|
||||||
|
const filename = 'D:\\xc_test_file.txt';
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 1. 开始上传
|
||||||
|
console.log('1. 开始上传...');
|
||||||
|
const startUrl = new URL('/api/files/upload/start', BASE_URL);
|
||||||
|
startUrl.searchParams.set('password', PASSWORD);
|
||||||
|
|
||||||
|
const startRes = await new Promise((resolve, reject) => {
|
||||||
|
const req = http.request({
|
||||||
|
hostname: startUrl.hostname,
|
||||||
|
port: startUrl.port,
|
||||||
|
path: startUrl.pathname + startUrl.search,
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' }
|
||||||
|
}, (res) => {
|
||||||
|
let data = '';
|
||||||
|
res.on('data', chunk => data += chunk);
|
||||||
|
res.on('end', () => {
|
||||||
|
try { resolve({ status: res.statusCode, data: JSON.parse(data) }); }
|
||||||
|
catch { resolve({ status: res.statusCode, data }); }
|
||||||
|
});
|
||||||
|
});
|
||||||
|
req.on('error', reject);
|
||||||
|
req.write(JSON.stringify({
|
||||||
|
filename: filename,
|
||||||
|
totalChunks: 1,
|
||||||
|
fileSize: Buffer.byteLength(testContent)
|
||||||
|
}));
|
||||||
|
req.end();
|
||||||
|
});
|
||||||
|
console.log('开始上传响应:', startRes.data);
|
||||||
|
|
||||||
|
if (!startRes.data.fileId) {
|
||||||
|
console.error('未获取到fileId');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const fileId = startRes.data.fileId;
|
||||||
|
|
||||||
|
// 2. 上传分块
|
||||||
|
console.log('2. 上传分块...');
|
||||||
|
const chunk = Buffer.from(testContent);
|
||||||
|
|
||||||
|
const chunkUrl = new URL('/api/files/upload/chunk', BASE_URL);
|
||||||
|
chunkUrl.searchParams.set('password', PASSWORD);
|
||||||
|
|
||||||
|
const FormData = require('form-data');
|
||||||
|
const form = new FormData();
|
||||||
|
form.append('fileId', fileId);
|
||||||
|
form.append('chunkIndex', '0');
|
||||||
|
form.append('chunk', chunk, { filename: 'chunk', contentType: 'application/octet-stream' });
|
||||||
|
|
||||||
|
const chunkRes = await new Promise((resolve, reject) => {
|
||||||
|
const req = http.request({
|
||||||
|
hostname: chunkUrl.hostname,
|
||||||
|
port: chunkUrl.port,
|
||||||
|
path: chunkUrl.pathname + chunkUrl.search,
|
||||||
|
method: 'POST',
|
||||||
|
headers: form.getHeaders()
|
||||||
|
}, (res) => {
|
||||||
|
let data = '';
|
||||||
|
res.on('data', c => data += c);
|
||||||
|
res.on('end', () => {
|
||||||
|
try { resolve({ status: res.statusCode, data: JSON.parse(data) }); }
|
||||||
|
catch { resolve({ status: res.statusCode, data }); }
|
||||||
|
});
|
||||||
|
});
|
||||||
|
req.on('error', reject);
|
||||||
|
req.write(form.getBuffer());
|
||||||
|
req.end();
|
||||||
|
});
|
||||||
|
console.log('分块上传响应:', chunkRes.data);
|
||||||
|
|
||||||
|
// 3. 合并文件
|
||||||
|
console.log('3. 合并文件...');
|
||||||
|
const mergeUrl = new URL('/api/files/upload/merge', BASE_URL);
|
||||||
|
mergeUrl.searchParams.set('password', PASSWORD);
|
||||||
|
|
||||||
|
const mergeRes = await new Promise((resolve, reject) => {
|
||||||
|
const req = http.request({
|
||||||
|
hostname: mergeUrl.hostname,
|
||||||
|
port: mergeUrl.port,
|
||||||
|
path: mergeUrl.pathname + mergeUrl.search,
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' }
|
||||||
|
}, (res) => {
|
||||||
|
let data = '';
|
||||||
|
res.on('data', c => data += c);
|
||||||
|
res.on('end', () => {
|
||||||
|
try { resolve({ status: res.statusCode, data: JSON.parse(data) }); }
|
||||||
|
catch { resolve({ status: res.statusCode, data }); }
|
||||||
|
});
|
||||||
|
});
|
||||||
|
req.on('error', reject);
|
||||||
|
req.write(JSON.stringify({
|
||||||
|
fileId: fileId,
|
||||||
|
totalChunks: 1,
|
||||||
|
filename: filename
|
||||||
|
}));
|
||||||
|
req.end();
|
||||||
|
});
|
||||||
|
console.log('合并响应:', mergeRes.data);
|
||||||
|
|
||||||
|
return mergeRes.data.success === true;
|
||||||
|
} catch (err) {
|
||||||
|
console.error('错误:', err.message);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function testDownloadFromSystemDir() {
|
||||||
|
console.log('\n=== 测试3: 从系统目录下载 ===');
|
||||||
|
const filename = 'D:\\xc_test_file.txt';
|
||||||
|
|
||||||
|
try {
|
||||||
|
const encodedPath = encodeURIComponent(filename);
|
||||||
|
const res = await request({
|
||||||
|
method: 'GET',
|
||||||
|
path: `/api/files/${encodedPath}`
|
||||||
|
});
|
||||||
|
console.log('状态:', res.status);
|
||||||
|
console.log('响应类型:', typeof res.data);
|
||||||
|
return res.status === 200;
|
||||||
|
} catch (err) {
|
||||||
|
console.error('错误:', err.message);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function testDeleteFile() {
|
||||||
|
console.log('\n=== 测试4: 删除测试文件 ===');
|
||||||
|
const filename = 'D:\\xc_test_file.txt';
|
||||||
|
|
||||||
|
try {
|
||||||
|
const encodedPath = encodeURIComponent(filename);
|
||||||
|
const res = await request({
|
||||||
|
method: 'DELETE',
|
||||||
|
path: `/api/files/${encodedPath}`
|
||||||
|
});
|
||||||
|
console.log('状态:', res.status);
|
||||||
|
console.log('响应:', res.data);
|
||||||
|
return res.data.success === true || res.status === 200;
|
||||||
|
} catch (err) {
|
||||||
|
console.error('错误:', err.message);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function main() {
|
||||||
|
console.log('========================================');
|
||||||
|
console.log('远程文件传输功能测试');
|
||||||
|
console.log('目标服务器: 146.56.248.142:8080');
|
||||||
|
console.log('========================================');
|
||||||
|
|
||||||
|
const results = [];
|
||||||
|
|
||||||
|
// 测试1: 获取驱动器列表
|
||||||
|
results.push({ name: '获取驱动器列表', pass: await testGetDrives() });
|
||||||
|
|
||||||
|
// 测试2: 上传到系统目录
|
||||||
|
results.push({ name: '上传到系统目录', pass: await testUploadToSystemDir() });
|
||||||
|
|
||||||
|
// 测试3: 从系统目录下载
|
||||||
|
results.push({ name: '从系统目录下载', pass: await testDownloadFromSystemDir() });
|
||||||
|
|
||||||
|
// 测试4: 删除测试文件
|
||||||
|
results.push({ name: '删除测试文件', pass: await testDeleteFile() });
|
||||||
|
|
||||||
|
// 汇总结果
|
||||||
|
console.log('\n========================================');
|
||||||
|
console.log('测试结果汇总:');
|
||||||
|
console.log('========================================');
|
||||||
|
results.forEach(r => {
|
||||||
|
console.log(`${r.pass ? '✓' : '✗'} ${r.name}`);
|
||||||
|
});
|
||||||
|
|
||||||
|
const allPass = results.every(r => r.pass);
|
||||||
|
console.log(`\n总体结果: ${allPass ? '全部通过' : '存在失败'}`);
|
||||||
|
process.exit(allPass ? 0 : 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
main();
|
||||||
89
remote/test-full.js
Normal file
89
remote/test-full.js
Normal file
@@ -0,0 +1,89 @@
|
|||||||
|
const WebSocket = require('ws');
|
||||||
|
const fs = require('fs');
|
||||||
|
|
||||||
|
const ws = new WebSocket('ws://127.0.0.1:3003/ws?password=wzw20040525');
|
||||||
|
|
||||||
|
let fileId = 'upload_test_' + Date.now();
|
||||||
|
const testContent = Buffer.from('Hello World Test File Content');
|
||||||
|
|
||||||
|
ws.on('open', () => {
|
||||||
|
console.log('=== Connected, starting upload test ===');
|
||||||
|
|
||||||
|
ws.send(JSON.stringify({
|
||||||
|
type: 'fileUploadStart',
|
||||||
|
fileId: fileId,
|
||||||
|
filename: 'test.txt',
|
||||||
|
totalChunks: 1,
|
||||||
|
fileSize: testContent.length,
|
||||||
|
requestId: 'req1'
|
||||||
|
}));
|
||||||
|
});
|
||||||
|
|
||||||
|
ws.on('message', (data, isBinary) => {
|
||||||
|
if (isBinary) {
|
||||||
|
console.log('Binary data received');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const msg = JSON.parse(data.toString());
|
||||||
|
console.log('Received:', msg.type, msg.fileId || '');
|
||||||
|
|
||||||
|
if (msg.type === 'fileUploadStart') {
|
||||||
|
console.log('Session started, sending chunk...');
|
||||||
|
ws.send(JSON.stringify({
|
||||||
|
type: 'fileUploadChunk',
|
||||||
|
fileId: fileId,
|
||||||
|
chunkIndex: 0,
|
||||||
|
data: testContent.toString('base64'),
|
||||||
|
requestId: 'req2'
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (msg.type === 'fileUploadChunk') {
|
||||||
|
console.log('Chunk sent, sending merge...');
|
||||||
|
ws.send(JSON.stringify({
|
||||||
|
type: 'fileUploadMerge',
|
||||||
|
fileId: fileId,
|
||||||
|
filename: 'test.txt',
|
||||||
|
totalChunks: 1,
|
||||||
|
requestId: 'req3'
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (msg.type === 'fileUploadResult') {
|
||||||
|
console.log('=== Upload Result:', msg.success ? 'SUCCESS' : 'FAILED', msg.filename || msg.error);
|
||||||
|
|
||||||
|
if (msg.success) {
|
||||||
|
console.log('\n=== Testing download ===');
|
||||||
|
ws.send(JSON.stringify({
|
||||||
|
type: 'fileDownloadStart',
|
||||||
|
filename: 'test.txt',
|
||||||
|
filePath: 'test.txt',
|
||||||
|
allowSystem: true,
|
||||||
|
requestId: 'req4'
|
||||||
|
}));
|
||||||
|
} else {
|
||||||
|
ws.close();
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (msg.type === 'fileDownloadStart') {
|
||||||
|
console.log('Download started, size:', msg.size);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (msg.type === 'fileDownloadChunk') {
|
||||||
|
console.log('Download chunk:', msg.chunkIndex, 'progress:', msg.progress + '%');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (msg.type === 'fileDownloadComplete') {
|
||||||
|
console.log('=== Download Result:', msg.success ? 'SUCCESS' : 'FAILED');
|
||||||
|
console.log('=== ALL TESTS PASSED ===');
|
||||||
|
ws.close();
|
||||||
|
process.exit(0);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
ws.on('error', (err) => { console.error('Error:', err.message); });
|
||||||
|
|
||||||
|
setTimeout(() => { console.log('=== TIMEOUT ==='); process.exit(1); }, 20000);
|
||||||
13
remote/test-ports.js
Normal file
13
remote/test-ports.js
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
const WebSocket = require('ws');
|
||||||
|
|
||||||
|
console.log('Testing port 8080...');
|
||||||
|
const ws1 = new WebSocket('ws://146.56.248.142:8080/ws?password=wzw20040525');
|
||||||
|
ws1.on('open', () => console.log('8080: Connected'));
|
||||||
|
ws1.on('error', () => console.log('8080: Failed'));
|
||||||
|
ws1.on('close', () => {
|
||||||
|
console.log('Testing port 8083...');
|
||||||
|
const ws2 = new WebSocket('ws://146.56.248.142:8083/ws?password=wzw20040525');
|
||||||
|
ws2.on('open', () => console.log('8083: Connected'));
|
||||||
|
ws2.on('error', () => console.log('8083: Failed'));
|
||||||
|
setTimeout(() => process.exit(0), 3000);
|
||||||
|
});
|
||||||
71
remote/test-remote.js
Normal file
71
remote/test-remote.js
Normal file
@@ -0,0 +1,71 @@
|
|||||||
|
const WebSocket = require('ws');
|
||||||
|
|
||||||
|
const ws = new WebSocket('ws://146.56.248.142:8083/ws?password=wzw20040525');
|
||||||
|
|
||||||
|
let fileId = 'test_' + Date.now();
|
||||||
|
const testContent = Buffer.from('Hello Remote Test');
|
||||||
|
|
||||||
|
ws.on('open', () => {
|
||||||
|
console.log('=== Connected to 146.56.248.142:8083 ===');
|
||||||
|
ws.send(JSON.stringify({
|
||||||
|
type: 'fileUploadStart',
|
||||||
|
fileId: fileId,
|
||||||
|
filename: 'test.txt',
|
||||||
|
totalChunks: 1,
|
||||||
|
fileSize: testContent.length,
|
||||||
|
requestId: 'req1'
|
||||||
|
}));
|
||||||
|
});
|
||||||
|
|
||||||
|
ws.on('message', (data, isBinary) => {
|
||||||
|
if (isBinary) return;
|
||||||
|
const msg = JSON.parse(data.toString());
|
||||||
|
console.log('Received:', msg.type);
|
||||||
|
|
||||||
|
if (msg.type === 'fileUploadStart') {
|
||||||
|
ws.send(JSON.stringify({
|
||||||
|
type: 'fileUploadChunk',
|
||||||
|
fileId: fileId,
|
||||||
|
chunkIndex: 0,
|
||||||
|
data: testContent.toString('base64'),
|
||||||
|
requestId: 'req2'
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (msg.type === 'fileUploadChunk') {
|
||||||
|
ws.send(JSON.stringify({
|
||||||
|
type: 'fileUploadMerge',
|
||||||
|
fileId: fileId,
|
||||||
|
filename: 'test.txt',
|
||||||
|
totalChunks: 1,
|
||||||
|
requestId: 'req3'
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (msg.type === 'fileUploadResult') {
|
||||||
|
console.log('=== Upload:', msg.success ? 'SUCCESS' : 'FAILED');
|
||||||
|
if (msg.success) {
|
||||||
|
ws.send(JSON.stringify({
|
||||||
|
type: 'fileDownloadStart',
|
||||||
|
filename: 'test.txt',
|
||||||
|
filePath: 'test.txt',
|
||||||
|
allowSystem: false,
|
||||||
|
requestId: 'req4'
|
||||||
|
}));
|
||||||
|
} else {
|
||||||
|
ws.close();
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (msg.type === 'fileDownloadComplete') {
|
||||||
|
console.log('=== Download:', msg.success ? 'SUCCESS' : 'FAILED');
|
||||||
|
console.log('=== ALL TESTS PASSED ===');
|
||||||
|
ws.close();
|
||||||
|
process.exit(0);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
ws.on('error', err => console.error('Error:', err.message));
|
||||||
|
|
||||||
|
setTimeout(() => { console.log('TIMEOUT'); process.exit(1); }, 15000);
|
||||||
30
remote/test-simple.js
Normal file
30
remote/test-simple.js
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
const WebSocket = require('ws');
|
||||||
|
|
||||||
|
console.log('Starting...');
|
||||||
|
const ws = new WebSocket('ws://146.56.248.142:8083/ws?password=wzw20040525');
|
||||||
|
|
||||||
|
ws.on('open', () => {
|
||||||
|
console.log('Connected!');
|
||||||
|
|
||||||
|
let fileId = 'test_' + Date.now();
|
||||||
|
const testContent = Buffer.from('Hello');
|
||||||
|
|
||||||
|
console.log('Sending fileUploadStart...');
|
||||||
|
ws.send(JSON.stringify({
|
||||||
|
type: 'fileUploadStart',
|
||||||
|
fileId: fileId,
|
||||||
|
filename: 'F:/test.txt',
|
||||||
|
totalChunks: 1,
|
||||||
|
fileSize: 5,
|
||||||
|
requestId: 'req1'
|
||||||
|
}));
|
||||||
|
});
|
||||||
|
|
||||||
|
ws.on('message', (data, isBinary) => {
|
||||||
|
console.log('Got message:', isBinary ? 'binary' : data.toString().substring(0,80));
|
||||||
|
});
|
||||||
|
|
||||||
|
ws.on('error', e => console.log('Error:', e.message));
|
||||||
|
ws.on('close', () => console.log('Closed'));
|
||||||
|
|
||||||
|
setTimeout(() => { console.log('Timeout'); process.exit(1); }, 10000);
|
||||||
57
remote/test-upload.js
Normal file
57
remote/test-upload.js
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
const WebSocket = require('ws');
|
||||||
|
|
||||||
|
console.log('Testing upload to remote 8083...');
|
||||||
|
const ws = new WebSocket('ws://146.56.248.142:8083/ws?password=wzw20040525');
|
||||||
|
|
||||||
|
let fileId = 'test_' + Date.now();
|
||||||
|
const testContent = Buffer.from('Hello Test 中文');
|
||||||
|
|
||||||
|
ws.on('open', () => {
|
||||||
|
console.log('Connected, sending fileUploadStart with F:/xxx.txt...');
|
||||||
|
ws.send(JSON.stringify({
|
||||||
|
type: 'fileUploadStart',
|
||||||
|
fileId: fileId,
|
||||||
|
filename: 'F:/小问题.txt',
|
||||||
|
totalChunks: 1,
|
||||||
|
fileSize: testContent.length,
|
||||||
|
requestId: 'req1'
|
||||||
|
}));
|
||||||
|
});
|
||||||
|
|
||||||
|
ws.on('message', (data, isBinary) => {
|
||||||
|
if (isBinary) return;
|
||||||
|
const msg = JSON.parse(data.toString());
|
||||||
|
console.log('Received:', msg.type);
|
||||||
|
|
||||||
|
if (msg.type === 'fileUploadStart') {
|
||||||
|
console.log('Session started, sending chunk...');
|
||||||
|
ws.send(JSON.stringify({
|
||||||
|
type: 'fileUploadChunk',
|
||||||
|
fileId: fileId,
|
||||||
|
chunkIndex: 0,
|
||||||
|
data: testContent.toString('base64'),
|
||||||
|
requestId: 'req2'
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (msg.type === 'fileUploadChunk') {
|
||||||
|
console.log('Chunk sent, sending merge...');
|
||||||
|
ws.send(JSON.stringify({
|
||||||
|
type: 'fileUploadMerge',
|
||||||
|
fileId: fileId,
|
||||||
|
filename: 'F:/小问题.txt',
|
||||||
|
totalChunks: 1,
|
||||||
|
requestId: 'req3'
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (msg.type === 'fileUploadResult') {
|
||||||
|
console.log('=== Upload Result:', msg.success ? 'SUCCESS' : 'FAILED', msg.error || '');
|
||||||
|
ws.close();
|
||||||
|
process.exit(0);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
ws.on('error', err => console.error('Error:', err.message));
|
||||||
|
|
||||||
|
setTimeout(() => { console.log('Timeout'); process.exit(1); }, 15000);
|
||||||
32
remote/test-ws.js
Normal file
32
remote/test-ws.js
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
const WebSocket = require('ws');
|
||||||
|
const ws = new WebSocket('ws://127.0.0.1:3001/ws?password=wzw20040525');
|
||||||
|
|
||||||
|
ws.on('open', () => {
|
||||||
|
console.log('Connected, sending fileBrowse...');
|
||||||
|
ws.send(JSON.stringify({ type: 'fileBrowse', path: 'C:\\', allowSystem: true, requestId: 'test1' }));
|
||||||
|
});
|
||||||
|
|
||||||
|
ws.on('message', (data) => {
|
||||||
|
console.log('Received raw:', data.toString().substring(0, 200));
|
||||||
|
|
||||||
|
if (Buffer.isBuffer(data) || data instanceof ArrayBuffer) {
|
||||||
|
console.log('Binary data');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const msg = JSON.parse(data);
|
||||||
|
console.log('Received:', msg.type);
|
||||||
|
if (msg.type === 'fileBrowseResult') {
|
||||||
|
console.log('Items:', msg.items?.slice(0,3));
|
||||||
|
ws.close();
|
||||||
|
process.exit(0);
|
||||||
|
}
|
||||||
|
} catch(e) {
|
||||||
|
console.log('Parse error:', e.message);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
ws.on('error', (err) => { console.error('Error:', err.message); });
|
||||||
|
|
||||||
|
setTimeout(() => { console.log('Timeout'); process.exit(1); }, 10000);
|
||||||
190
src/modules/remote/ws.ts
Normal file
190
src/modules/remote/ws.ts
Normal file
@@ -0,0 +1,190 @@
|
|||||||
|
type MessageHandler = (data: any) => void
|
||||||
|
|
||||||
|
interface PendingRequest {
|
||||||
|
resolve: (value: any) => void
|
||||||
|
reject: (reason?: any) => void
|
||||||
|
timeout: NodeJS.Timeout
|
||||||
|
}
|
||||||
|
|
||||||
|
class RemoteWebSocket {
|
||||||
|
private ws: WebSocket | null = null
|
||||||
|
private url: string = ''
|
||||||
|
private password: string = ''
|
||||||
|
private pendingRequests: Map<string, PendingRequest> = new Map()
|
||||||
|
private messageHandlers: Map<string, MessageHandler[]> = new Map()
|
||||||
|
private reconnectTimer: NodeJS.Timeout | null = null
|
||||||
|
private isManualClose: boolean = false
|
||||||
|
|
||||||
|
connect(serverHost: string, port: number, password?: string): Promise<void> {
|
||||||
|
this.url = `ws://${serverHost}:${port}/ws`
|
||||||
|
this.password = password || ''
|
||||||
|
this.isManualClose = false
|
||||||
|
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
try {
|
||||||
|
const wsUrl = this.password
|
||||||
|
? `${this.url}?password=${encodeURIComponent(this.password)}`
|
||||||
|
: this.url
|
||||||
|
|
||||||
|
this.ws = new WebSocket(wsUrl)
|
||||||
|
|
||||||
|
this.ws.onopen = () => {
|
||||||
|
console.log('Remote WebSocket connected')
|
||||||
|
resolve()
|
||||||
|
}
|
||||||
|
|
||||||
|
this.ws.onclose = () => {
|
||||||
|
console.log('Remote WebSocket disconnected')
|
||||||
|
if (!this.isManualClose) {
|
||||||
|
this.scheduleReconnect()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.ws.onerror = (error) => {
|
||||||
|
console.error('Remote WebSocket error:', error)
|
||||||
|
reject(error)
|
||||||
|
}
|
||||||
|
|
||||||
|
this.ws.onmessage = (event) => {
|
||||||
|
try {
|
||||||
|
if (event.data instanceof Blob || event.data instanceof ArrayBuffer) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
this.handleMessage(JSON.parse(event.data))
|
||||||
|
} catch (e) {
|
||||||
|
console.error('WebSocket message parse error:', e, event.data)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
reject(error)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
private scheduleReconnect() {
|
||||||
|
if (this.reconnectTimer) return
|
||||||
|
|
||||||
|
this.reconnectTimer = setTimeout(() => {
|
||||||
|
this.reconnectTimer = null
|
||||||
|
if (!this.isManualClose && this.url) {
|
||||||
|
console.log('Attempting to reconnect Remote WebSocket...')
|
||||||
|
this.connect(
|
||||||
|
this.url.replace('ws://', '').split(':')[0],
|
||||||
|
parseInt(this.url.replace('ws://', '').split(':')[1] || '80'),
|
||||||
|
this.password
|
||||||
|
).catch(() => {})
|
||||||
|
}
|
||||||
|
}, 3000)
|
||||||
|
}
|
||||||
|
|
||||||
|
disconnect() {
|
||||||
|
this.isManualClose = true
|
||||||
|
if (this.reconnectTimer) {
|
||||||
|
clearTimeout(this.reconnectTimer)
|
||||||
|
this.reconnectTimer = null
|
||||||
|
}
|
||||||
|
if (this.ws) {
|
||||||
|
this.ws.close()
|
||||||
|
this.ws = null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
send(message: any): Promise<any> {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
if (!this.ws || this.ws.readyState !== WebSocket.OPEN) {
|
||||||
|
reject(new Error('WebSocket not connected'))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const requestId = message.type + '_' + Date.now() + '_' + Math.random()
|
||||||
|
message.requestId = requestId
|
||||||
|
|
||||||
|
const timeout = setTimeout(() => {
|
||||||
|
this.pendingRequests.delete(requestId)
|
||||||
|
reject(new Error('Request timeout'))
|
||||||
|
}, 30000)
|
||||||
|
|
||||||
|
this.pendingRequests.set(requestId, { resolve, reject, timeout })
|
||||||
|
|
||||||
|
this.ws.send(JSON.stringify(message))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
on(type: string, handler: MessageHandler) {
|
||||||
|
if (!this.messageHandlers.has(type)) {
|
||||||
|
this.messageHandlers.set(type, [])
|
||||||
|
}
|
||||||
|
this.messageHandlers.get(type)!.push(handler)
|
||||||
|
}
|
||||||
|
|
||||||
|
off(type: string, handler: MessageHandler) {
|
||||||
|
const handlers = this.messageHandlers.get(type)
|
||||||
|
if (handlers) {
|
||||||
|
const index = handlers.indexOf(handler)
|
||||||
|
if (index > -1) {
|
||||||
|
handlers.splice(index, 1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private handleMessage(data: any) {
|
||||||
|
const requestId = data.requestId
|
||||||
|
|
||||||
|
if (requestId && this.pendingRequests.has(requestId)) {
|
||||||
|
const { resolve, timeout } = this.pendingRequests.get(requestId)!
|
||||||
|
clearTimeout(timeout)
|
||||||
|
this.pendingRequests.delete(requestId)
|
||||||
|
resolve(data)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const handlers = this.messageHandlers.get(data.type)
|
||||||
|
if (handlers) {
|
||||||
|
handlers.forEach(handler => handler(data))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
isConnected(): boolean {
|
||||||
|
return this.ws !== null && this.ws.readyState === WebSocket.OPEN
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let instance: RemoteWebSocket | null = null
|
||||||
|
let lastServerHost: string | null = null
|
||||||
|
let lastPort: number | null = null
|
||||||
|
|
||||||
|
export const getRemoteWebSocket = (
|
||||||
|
serverHost?: string,
|
||||||
|
port?: number,
|
||||||
|
password?: string
|
||||||
|
): RemoteWebSocket => {
|
||||||
|
if (!instance) {
|
||||||
|
instance = new RemoteWebSocket()
|
||||||
|
}
|
||||||
|
|
||||||
|
if (serverHost && port) {
|
||||||
|
const serverChanged = serverHost !== lastServerHost || port !== lastPort
|
||||||
|
if (serverChanged) {
|
||||||
|
instance.disconnect()
|
||||||
|
lastServerHost = serverHost
|
||||||
|
lastPort = port
|
||||||
|
instance.connect(serverHost, port, password).catch(console.error)
|
||||||
|
} else if (!instance.isConnected()) {
|
||||||
|
instance.connect(serverHost, port, password).catch(console.error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return instance
|
||||||
|
}
|
||||||
|
|
||||||
|
export const createRemoteWebSocket = (
|
||||||
|
serverHost: string,
|
||||||
|
port: number,
|
||||||
|
password?: string
|
||||||
|
): RemoteWebSocket => {
|
||||||
|
const ws = new RemoteWebSocket()
|
||||||
|
ws.connect(serverHost, port, password).catch(console.error)
|
||||||
|
return ws
|
||||||
|
}
|
||||||
|
|
||||||
|
export { RemoteWebSocket }
|
||||||
Reference in New Issue
Block a user