From 6d5520dfa59b0c4c0a6fdf9d6374b2543a0fc6e2 Mon Sep 17 00:00:00 2001 From: ssdfasd <2156608475@qq.com> Date: Tue, 10 Mar 2026 00:34:02 +0800 Subject: [PATCH] =?UTF-8?q?feat(remote):=20=E6=96=87=E4=BB=B6=E4=BC=A0?= =?UTF-8?q?=E8=BE=93=E6=94=B9=E7=94=A8Electron=20IPC=E9=80=9A=E9=81=93?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 主进程新增4个IPC handler处理远程文件操作 - 前端通过IPC调用而非浏览器fetch访问远程API - Remote服务新增3003端口专门处理文件传输 - 上传使用文件路径方案,下载使用保存对话框方案 --- electron/main.ts | 136 ++++++++++++++++++ electron/preload.ts | 8 ++ remote/src/core/App.js | 1 + remote/src/server/Server.js | 29 +++- src/modules/remote/api.ts | 116 ++++----------- .../file-transfer/FileTransferPage.tsx | 9 +- src/types/electron.d.ts | 20 +++ 7 files changed, 221 insertions(+), 98 deletions(-) diff --git a/electron/main.ts b/electron/main.ts index 3efae7d..27bd173 100644 --- a/electron/main.ts +++ b/electron/main.ts @@ -156,6 +156,142 @@ ipcMain.handle('clipboard-write-text', async (event, text: string) => { } }); +ipcMain.handle('remote-fetch-drives', async (_event, serverHost: string, port: number, password?: string) => { + 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: string; isDirectory: boolean; size: number }) => ({ + name: item.name, + path: item.name, + type: item.isDirectory ? 'dir' : 'file', + size: item.size, + modified: '', + })) + }; + } catch (error: any) { + log.error('Remote fetch drives failed:', error); + return { success: false, error: error.message }; + } +}); + +ipcMain.handle('remote-fetch-files', async (_event, serverHost: string, port: number, filePath: string, password?: string) => { + 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: string; isDirectory: boolean; size: number; modified: Date }) => ({ + 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: any) { + log.error('Remote fetch files failed:', error); + return { success: false, error: error.message }; + } +}); + +ipcMain.handle('remote-upload-file', async (_event, serverHost: string, port: number, filePath: string, remotePath: string, password?: string) => { + try { + const win = electronState.getMainWindow(); + if (!win) { + throw new Error('No window found'); + } + + const fullPath = path.resolve(filePath); + if (!fs.existsSync(fullPath)) { + throw new Error('File not found'); + } + + const fileBuffer = fs.readFileSync(fullPath); + const fileName = path.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: any) { + log.error('Remote upload failed:', error); + return { success: false, error: error.message }; + } +}); + +ipcMain.handle('remote-download-file', async (_event, serverHost: string, port: number, fileName: string, remotePath: string, password?: string) => { + 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 dialog.showSaveDialog(win, { + title: '保存文件', + defaultPath: fileName, + }); + + if (!filePath) { + return { success: false, canceled: true }; + } + + fs.writeFileSync(filePath, Buffer.from(buffer)); + + return { success: true, filePath }; + } catch (error: any) { + log.error('Remote download failed:', error); + return { success: false, error: error.message }; + } +}); + async function startServer() { if (electronState.isDevelopment()) { log.info('In dev mode, assuming external servers are running.'); diff --git a/electron/preload.ts b/electron/preload.ts index 460f0dd..19da315 100644 --- a/electron/preload.ts +++ b/electron/preload.ts @@ -21,4 +21,12 @@ contextBridge.exposeInMainWorld('electronAPI', { }, clipboardReadText: () => ipcRenderer.invoke('clipboard-read-text'), clipboardWriteText: (text: string) => ipcRenderer.invoke('clipboard-write-text', text), + remoteFetchDrives: (serverHost: string, port: number, password?: string) => + ipcRenderer.invoke('remote-fetch-drives', serverHost, port, password), + remoteFetchFiles: (serverHost: string, port: number, filePath: string, password?: string) => + ipcRenderer.invoke('remote-fetch-files', serverHost, port, filePath, password), + remoteUploadFile: (serverHost: string, port: number, filePath: string, remotePath: string, password?: string) => + ipcRenderer.invoke('remote-upload-file', serverHost, port, filePath, remotePath, password), + remoteDownloadFile: (serverHost: string, port: number, fileName: string, remotePath: string, password?: string) => + ipcRenderer.invoke('remote-download-file', serverHost, port, fileName, remotePath, password), }) diff --git a/remote/src/core/App.js b/remote/src/core/App.js index 8313a96..b226bfd 100644 --- a/remote/src/core/App.js +++ b/remote/src/core/App.js @@ -145,6 +145,7 @@ class App { const serverConfig = config.getSection('server') || {}; return new Server({ port: serverConfig.port || 3000, + fileTransferPort: serverConfig.fileTransferPort || 3003, host: serverConfig.host || '0.0.0.0' }); }); diff --git a/remote/src/server/Server.js b/remote/src/server/Server.js index 3b7de2e..f992540 100644 --- a/remote/src/server/Server.js +++ b/remote/src/server/Server.js @@ -5,9 +5,11 @@ const path = require('path'); class Server { constructor(config = {}) { this.port = config.port || 3000; + this.fileTransferPort = config.fileTransferPort || 3003; this.host = config.host || '0.0.0.0'; this.app = express(); this.server = http.createServer(this.app); + this.fileTransferServer = null; } use(...args) { @@ -28,20 +30,33 @@ class Server { start() { return new Promise((resolve, reject) => { this.server.listen({ port: this.port, host: this.host }, () => { - resolve(this.getAddress()); + console.log(`Server started on port ${this.port}`); }); this.server.on('error', reject); + + this.fileTransferServer = http.createServer(this.app); + this.fileTransferServer.listen({ port: this.fileTransferPort, host: this.host }, () => { + console.log(`File transfer server started on port ${this.fileTransferPort}`); + }); + + resolve(this.getAddress()); }); } stop() { return new Promise((resolve, reject) => { - this.server.close((err) => { - if (err) { - reject(err); - } else { - resolve(); - } + const closeFileTransfer = this.fileTransferServer + ? new Promise((res) => this.fileTransferServer.close(res)) + : Promise.resolve(); + + closeFileTransfer.then(() => { + this.server.close((err) => { + if (err) { + reject(err); + } else { + resolve(); + } + }); }); }); } diff --git a/src/modules/remote/api.ts b/src/modules/remote/api.ts index e72de0b..3d0a103 100644 --- a/src/modules/remote/api.ts +++ b/src/modules/remote/api.ts @@ -68,6 +68,18 @@ export const saveDeviceData = async (deviceName: string, lastConnected: string): await getApi().post('saveData', { deviceName, lastConnected }) } +export const fetchRemoteDrives = async ( + serverHost: string, + port: number, + password?: string +): Promise => { + const result = await window.electronAPI.remoteFetchDrives(serverHost, port, password) + if (!result.success) { + throw new Error(result.error || 'Failed to fetch drives') + } + return result.data +} + export const fetchRemoteFiles = async ( serverHost: string, port: number, @@ -75,92 +87,27 @@ export const fetchRemoteFiles = async ( password?: string, allowSystem: boolean = true ): Promise => { - let url = `http://${serverHost}:${port}/api/files/browse?path=${encodeURIComponent(path)}&allowSystem=${allowSystem}` - if (password) { - url += `&password=${encodeURIComponent(password)}` + const result = await window.electronAPI.remoteFetchFiles(serverHost, port, path, password) + if (!result.success) { + throw new Error(result.error || 'Failed to fetch files') } - const response = await fetch(url) - if (!response.ok) { - throw new Error(`Failed to fetch remote files: ${response.statusText}`) - } - const data = await response.json() - const items = data.items || [] - return items.map((item: { name: string; isDirectory: boolean; size: number; modified: Date }) => ({ - name: item.name, - path: data.currentPath ? `${data.currentPath}/${item.name}` : item.name, - type: item.isDirectory ? 'dir' : 'file', - size: item.size, - modified: item.modified?.toString(), - })) -} - -export const fetchRemoteDrives = async ( - serverHost: string, - port: number, - password?: string -): Promise => { - 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 remote drives: ${response.statusText}`) - } - const data = await response.json() - const items = data.items || [] - return items.map((item: { name: string; isDirectory: boolean; size: number }) => ({ - name: item.name, - path: item.name, - type: item.isDirectory ? 'dir' : 'file', - size: item.size, - modified: '', - })) + return result.data } export const uploadFileToRemote = async ( serverHost: string, port: number, - file: File, + filePath: string, remotePath: string, password?: string, onProgress?: (progress: number) => void ): Promise => { - let url = `http://${serverHost}:${port}/api/files/upload` - if (password) { - url += `?password=${encodeURIComponent(password)}` + onProgress?.(50) + const result = await window.electronAPI.remoteUploadFile(serverHost, port, filePath, remotePath, password) + onProgress?.(100) + if (!result.success) { + throw new Error(result.error || 'Upload failed') } - const formData = new FormData() - formData.append('file', file) - if (remotePath) { - formData.append('path', remotePath) - } - - const xhr = new XMLHttpRequest() - - return new Promise((resolve, reject) => { - xhr.upload.addEventListener('progress', (event) => { - if (event.lengthComputable && onProgress) { - const progress = Math.round((event.loaded / event.total) * 100) - onProgress(progress) - } - }) - - xhr.addEventListener('load', () => { - if (xhr.status >= 200 && xhr.status < 300) { - resolve() - } else { - reject(new Error(`Upload failed: ${xhr.statusText}`)) - } - }) - - xhr.addEventListener('error', () => { - reject(new Error('Upload failed')) - }) - - xhr.open('POST', url) - xhr.send(formData) - }) } export const downloadFileFromRemote = async ( @@ -170,15 +117,14 @@ export const downloadFileFromRemote = async ( remotePath: string, password?: string, onProgress?: (progress: number) => void -): Promise => { - let url = `http://${serverHost}:${port}/api/files/${encodeURIComponent(fileName)}` - if (password) { - url += `?password=${encodeURIComponent(password)}` +): Promise => { + onProgress?.(50) + const result = await window.electronAPI.remoteDownloadFile(serverHost, port, fileName, remotePath, password) + onProgress?.(100) + if (!result.success) { + if (result.canceled) { + return + } + throw new Error(result.error || 'Download failed') } - - const response = await fetch(url) - if (!response.ok) { - throw new Error(`Download failed: ${response.statusText}`) - } - return await response.blob() } diff --git a/src/modules/remote/components/file-transfer/FileTransferPage.tsx b/src/modules/remote/components/file-transfer/FileTransferPage.tsx index b22219a..253d6c8 100644 --- a/src/modules/remote/components/file-transfer/FileTransferPage.tsx +++ b/src/modules/remote/components/file-transfer/FileTransferPage.tsx @@ -1,6 +1,6 @@ import React, { useState, useCallback, useRef, useEffect } from 'react' import type { FileItem } from '@/lib/api' -import { type RemoteFileItem, uploadFileToRemote, downloadFileFromRemote, fetchSystemFileContent } from '../../api' +import { type RemoteFileItem, uploadFileToRemote, downloadFileFromRemote } from '../../api' import { type TransferItem } from '../../types' import { LocalFilePanel } from './LocalFilePanel' import { RemoteFilePanel } from './RemoteFilePanel' @@ -40,10 +40,7 @@ export const FileTransferPage: React.FC = ({ serverHost, setTransfers((prev) => [...prev, newTransfer]) try { - const blob = await fetchSystemFileContent(localSelected.path) - const file = new File([blob], localSelected.name, { type: blob.type }) - - await uploadFileToRemote(serverHost, port, file, remotePath, password, (progress) => { + await uploadFileToRemote(serverHost, port, localSelected.path, remotePath, password, (progress) => { setTransfers((prev) => prev.map((t) => (t.id === transferId ? { ...t, progress } : t)) ) @@ -67,7 +64,7 @@ export const FileTransferPage: React.FC = ({ serverHost, } finally { setTransferring(false) } - }, [localSelected, serverHost, port]) + }, [localSelected, serverHost, port, remotePath, password]) const handleDownload = useCallback(async () => { if (!remoteSelected) return diff --git a/src/types/electron.d.ts b/src/types/electron.d.ts index 8edb430..6a5cbe3 100644 --- a/src/types/electron.d.ts +++ b/src/types/electron.d.ts @@ -16,6 +16,26 @@ export interface ElectronAPI { onRemoteClipboardAutoSync: (callback: (text: string) => void) => () => void clipboardReadText: () => Promise<{ success: boolean; text?: string; error?: string }> clipboardWriteText: (text: string) => Promise<{ success: boolean; error?: string }> + remoteFetchDrives: (serverHost: string, port: number, password?: string) => Promise<{ + success: boolean + data?: Array<{ name: string; path: string; type: 'file' | 'dir'; size: number; modified: string }> + error?: string + }> + remoteFetchFiles: (serverHost: string, port: number, filePath: string, password?: string) => Promise<{ + success: boolean + data?: Array<{ name: string; path: string; type: 'file' | 'dir'; size: number; modified?: string }> + error?: string + }> + remoteUploadFile: (serverHost: string, port: number, filePath: string, remotePath: string, password?: string) => Promise<{ + success: boolean + error?: string + }> + remoteDownloadFile: (serverHost: string, port: number, fileName: string, remotePath: string, password?: string) => Promise<{ + success: boolean + filePath?: string + error?: string + canceled?: boolean + }> } declare global {