From de4c101b36afac8c57fdb31a0ff6d98e416a6baa Mon Sep 17 00:00:00 2001 From: ssdfasd <2156608475@qq.com> Date: Tue, 10 Mar 2026 15:36:10 +0800 Subject: [PATCH] =?UTF-8?q?feat(remote):=20=E5=AE=9E=E7=8E=B0=E6=96=87?= =?UTF-8?q?=E4=BB=B6=E4=B8=8A=E4=BC=A0=E7=9C=9F=E5=AE=9E=E8=BF=9B=E5=BA=A6?= =?UTF-8?q?=E6=98=BE=E7=A4=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 使用分块上传替代一次性上传 - 调用 /upload/start → /upload/chunk → /upload/merge 接口 - 通过 IPC 事件实时推送上传进度到前端 - 修复 merge 时未使用目标路径的问题 --- dist-electron/main.js | 56 ++++++++++++++++----- dist-electron/main.js.map | 2 +- dist-electron/preload.cjs | 7 ++- dist-electron/preload.cjs.map | 2 +- electron/main.ts | 66 +++++++++++++++++++------ electron/preload.ts | 9 +++- remote/src/routes/files.js | 4 +- remote/src/services/file/FileService.js | 8 ++- src/modules/remote/api.ts | 27 ++++++++-- src/types/electron.d.ts | 3 +- 10 files changed, 142 insertions(+), 42 deletions(-) diff --git a/dist-electron/main.js b/dist-electron/main.js index 77fd3ac..8ce144e 100644 --- a/dist-electron/main.js +++ b/dist-electron/main.js @@ -316,7 +316,7 @@ ipcMain.handle("remote-fetch-files", async (_event, serverHost, port, filePath, return { success: false, error: error.message }; } }); -ipcMain.handle("remote-upload-file", async (_event, serverHost, port, filePath, remotePath, password) => { +ipcMain.handle("remote-upload-file", async (_event, id, serverHost, port, filePath, remotePath, password) => { try { const win = electronState.getMainWindow(); if (!win) { @@ -326,24 +326,54 @@ ipcMain.handle("remote-upload-file", async (_event, serverHost, port, filePath, if (!fs2.existsSync(fullPath)) { throw new Error("File not found"); } - const fileBuffer = fs2.readFileSync(fullPath); + const stats = fs2.statSync(fullPath); + const fileSize = stats.size; const fileName = path2.basename(fullPath); - let url = `http://${serverHost}:${port}/api/files/upload`; + let url = `http://${serverHost}:${port}/api/files/upload/start`; 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, { + const startResponse = await fetch(url, { method: "POST", - body: formData + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ filename: fileName, fileSize }) }); - if (!response.ok) { - throw new Error(`Upload failed: ${response.statusText}`); + if (!startResponse.ok) { + throw new Error(`Failed to start upload: ${startResponse.statusText}`); + } + const { fileId, chunkSize } = await startResponse.json(); + const CHUNK_SIZE = chunkSize || 64 * 1024; + const totalChunks = Math.ceil(fileSize / CHUNK_SIZE); + const readStream = fs2.createReadStream(fullPath, { highWaterMark: CHUNK_SIZE }); + let chunkIndex = 0; + let uploadedBytes = 0; + for await (const chunk of readStream) { + const formData = new FormData(); + const blob = new Blob([chunk]); + formData.append("chunk", blob, fileName); + formData.append("fileId", fileId); + formData.append("chunkIndex", chunkIndex.toString()); + const chunkUrl = `http://${serverHost}:${port}/api/files/upload/chunk${password ? `?password=${encodeURIComponent(password)}` : ""}`; + const chunkResponse = await fetch(chunkUrl, { + method: "POST", + body: formData + }); + if (!chunkResponse.ok) { + throw new Error(`Failed to upload chunk ${chunkIndex}: ${chunkResponse.statusText}`); + } + uploadedBytes += chunk.length; + const progress = Math.round(uploadedBytes / fileSize * 100); + win.webContents.send("upload-progress", { id, progress }); + chunkIndex++; + } + const mergeUrl = `http://${serverHost}:${port}/api/files/upload/merge${password ? `?password=${encodeURIComponent(password)}` : ""}`; + const mergeResponse = await fetch(mergeUrl, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ fileId, totalChunks, filename: fileName, path: remotePath }) + }); + if (!mergeResponse.ok) { + throw new Error(`Failed to merge chunks: ${mergeResponse.statusText}`); } return { success: true }; } catch (error) { diff --git a/dist-electron/main.js.map b/dist-electron/main.js.map index 3e04c87..f4a6a8f 100644 --- a/dist-electron/main.js.map +++ b/dist-electron/main.js.map @@ -1 +1 @@ -{"version":3,"sources":["../electron/main.ts","../electron/services/pdfGenerator.ts","../electron/services/htmlImport.ts","../electron/state.ts"],"sourcesContent":["import { app, BrowserWindow, shell, ipcMain, dialog, nativeTheme, globalShortcut, clipboard } from 'electron';\r\nimport path from 'path';\r\nimport { fileURLToPath, pathToFileURL } from 'url';\r\nimport fs from 'fs';\r\nimport log from 'electron-log';\r\nimport { generatePdf } from './services/pdfGenerator';\r\nimport { selectHtmlFile } from './services/htmlImport';\r\nimport { electronState } from './state';\r\n\r\nlog.initialize();\r\n\r\nconst __filename = fileURLToPath(import.meta.url);\r\nconst __dirname = path.dirname(__filename);\r\n\r\nprocess.env.NOTEBOOK_ROOT = path.join(app.getPath('documents'), 'XCDesktop');\r\n\r\nif (!fs.existsSync(process.env.NOTEBOOK_ROOT)) {\r\n try {\r\n fs.mkdirSync(process.env.NOTEBOOK_ROOT, { recursive: true });\r\n } catch (err) {\r\n log.error('Failed to create notebook directory:', err);\r\n }\r\n}\r\n\r\nelectronState.setDevelopment(!app.isPackaged);\r\n\r\nlet lastClipboardText = '';\r\n\r\nfunction startClipboardWatcher() {\r\n lastClipboardText = clipboard.readText();\r\n \r\n setInterval(() => {\r\n try {\r\n const currentText = clipboard.readText();\r\n if (currentText && currentText !== lastClipboardText) {\r\n lastClipboardText = currentText;\r\n log.info('Clipboard changed, syncing to remote');\r\n const win = electronState.getMainWindow();\r\n if (win) {\r\n win.webContents.send('remote-clipboard-auto-sync', currentText);\r\n }\r\n }\r\n } catch (e) {\r\n // ignore\r\n }\r\n }, 1000);\r\n}\r\n\r\nasync function createWindow() {\r\n const initialSymbolColor = nativeTheme.shouldUseDarkColors ? '#ffffff' : '#000000';\r\n \r\n const mainWindow = new BrowserWindow({\r\n width: 1280,\r\n height: 800,\r\n minWidth: 1600,\r\n minHeight: 900,\r\n autoHideMenuBar: true,\r\n titleBarStyle: 'hidden',\r\n titleBarOverlay: {\r\n color: '#00000000',\r\n symbolColor: initialSymbolColor,\r\n height: 32,\r\n },\r\n webPreferences: {\r\n nodeIntegration: false,\r\n contextIsolation: true,\r\n sandbox: false,\r\n webviewTag: true,\r\n preload: path.join(__dirname, 'preload.cjs'),\r\n },\r\n });\r\n\r\n electronState.setMainWindow(mainWindow);\r\n mainWindow.setMenu(null);\r\n\r\n mainWindow.webContents.setWindowOpenHandler(({ url }) => {\r\n if (url.startsWith('http:') || url.startsWith('https:')) {\r\n shell.openExternal(url);\r\n return { action: 'deny' };\r\n }\r\n return { action: 'allow' };\r\n });\r\n\r\n if (electronState.isDevelopment()) {\r\n log.info('Loading development URL...');\r\n try {\r\n await mainWindow.loadURL('http://localhost:5173');\r\n } catch (e) {\r\n log.error('Failed to load dev URL. Make sure npm run electron:dev is used.', e);\r\n }\r\n mainWindow.webContents.openDevTools();\r\n } else {\r\n log.info(`Loading production URL with port ${electronState.getServerPort()}...`);\r\n await mainWindow.loadURL(`http://localhost:${electronState.getServerPort()}`);\r\n }\r\n}\r\n\r\nipcMain.handle('export-pdf', async (event, title, htmlContent) => {\r\n const win = BrowserWindow.fromWebContents(event.sender);\r\n if (!win) return { success: false, error: 'No window found' };\r\n\r\n try {\r\n const { filePath } = await dialog.showSaveDialog(win, {\r\n title: '导出 PDF',\r\n defaultPath: `${title}.pdf`,\r\n filters: [{ name: 'PDF Files', extensions: ['pdf'] }]\r\n });\r\n\r\n if (!filePath) return { success: false, canceled: true };\r\n\r\n if (!htmlContent) {\r\n throw new Error('No HTML content provided for PDF export');\r\n }\r\n\r\n const pdfData = await generatePdf(htmlContent);\r\n fs.writeFileSync(filePath, pdfData);\r\n\r\n return { success: true, filePath };\r\n } catch (error: any) {\r\n log.error('Export PDF failed:', error);\r\n return { success: false, error: error.message };\r\n }\r\n});\r\n\r\nipcMain.handle('select-html-file', async (event) => {\r\n const win = BrowserWindow.fromWebContents(event.sender);\r\n return selectHtmlFile(win);\r\n});\r\n\r\nipcMain.handle('update-titlebar-buttons', async (event, symbolColor: string) => {\r\n const win = BrowserWindow.fromWebContents(event.sender);\r\n if (win) {\r\n win.setTitleBarOverlay({ symbolColor });\r\n return { success: true };\r\n }\r\n return { success: false };\r\n});\r\n\r\nipcMain.handle('clipboard-read-text', async () => {\r\n try {\r\n const text = clipboard.readText();\r\n return { success: true, text };\r\n } catch (error: any) {\r\n log.error('Clipboard read failed:', error);\r\n return { success: false, error: error.message };\r\n }\r\n});\r\n\r\nipcMain.handle('clipboard-write-text', async (event, text: string) => {\r\n try {\r\n clipboard.writeText(text);\r\n return { success: true };\r\n } catch (error: any) {\r\n log.error('Clipboard write failed:', error);\r\n return { success: false, error: error.message };\r\n }\r\n});\r\n\r\nipcMain.handle('remote-fetch-drives', async (_event, serverHost: string, port: number, password?: string) => {\r\n try {\r\n let url = `http://${serverHost}:${port}/api/files/drives`;\r\n if (password) {\r\n url += `?password=${encodeURIComponent(password)}`;\r\n }\r\n const response = await fetch(url);\r\n if (!response.ok) {\r\n throw new Error(`Failed to fetch drives: ${response.statusText}`);\r\n }\r\n const data = await response.json();\r\n const items = data.items || [];\r\n return {\r\n success: true,\r\n data: items.map((item: { name: string; isDirectory: boolean; size: number }) => ({\r\n name: item.name,\r\n path: item.name,\r\n type: item.isDirectory ? 'dir' : 'file',\r\n size: item.size,\r\n modified: '',\r\n }))\r\n };\r\n } catch (error: any) {\r\n log.error('Remote fetch drives failed:', error);\r\n return { success: false, error: error.message };\r\n }\r\n});\r\n\r\nipcMain.handle('remote-fetch-files', async (_event, serverHost: string, port: number, filePath: string, password?: string) => {\r\n try {\r\n let url = `http://${serverHost}:${port}/api/files/browse?path=${encodeURIComponent(filePath)}&allowSystem=true`;\r\n if (password) {\r\n url += `&password=${encodeURIComponent(password)}`;\r\n }\r\n const response = await fetch(url);\r\n if (!response.ok) {\r\n throw new Error(`Failed to fetch files: ${response.statusText}`);\r\n }\r\n const data = await response.json();\r\n const items = data.items || [];\r\n return {\r\n success: true,\r\n data: items.map((item: { name: string; isDirectory: boolean; size: number; modified: Date }) => ({\r\n name: item.name,\r\n path: data.currentPath ? `${data.currentPath}/${item.name}` : item.name,\r\n type: item.isDirectory ? 'dir' : 'file',\r\n size: item.size,\r\n modified: item.modified?.toString(),\r\n }))\r\n };\r\n } catch (error: any) {\r\n log.error('Remote fetch files failed:', error);\r\n return { success: false, error: error.message };\r\n }\r\n});\r\n\r\nipcMain.handle('remote-upload-file', async (_event, serverHost: string, port: number, filePath: string, remotePath: string, password?: string) => {\r\n try {\r\n const win = electronState.getMainWindow();\r\n if (!win) {\r\n throw new Error('No window found');\r\n }\r\n\r\n const fullPath = path.resolve(filePath);\r\n if (!fs.existsSync(fullPath)) {\r\n throw new Error('File not found');\r\n }\r\n\r\n const fileBuffer = fs.readFileSync(fullPath);\r\n const fileName = path.basename(fullPath);\r\n\r\n let url = `http://${serverHost}:${port}/api/files/upload`;\r\n if (password) {\r\n url += `?password=${encodeURIComponent(password)}`;\r\n }\r\n\r\n const formData = new FormData();\r\n const blob = new Blob([fileBuffer]);\r\n formData.append('file', blob, fileName);\r\n if (remotePath) {\r\n formData.append('path', remotePath);\r\n }\r\n\r\n const response = await fetch(url, {\r\n method: 'POST',\r\n body: formData,\r\n });\r\n\r\n if (!response.ok) {\r\n throw new Error(`Upload failed: ${response.statusText}`);\r\n }\r\n\r\n return { success: true };\r\n } catch (error: any) {\r\n log.error('Remote upload failed:', error);\r\n return { success: false, error: error.message };\r\n }\r\n});\r\n\r\nipcMain.handle('remote-download-file', async (_event, id: string, serverHost: string, port: number, fileName: string, remotePath: string, localPath: string, password?: string) => {\r\n try {\r\n log.info('Remote download params:', { id, serverHost, port, fileName, remotePath, localPath, password });\r\n \r\n const win = electronState.getMainWindow();\r\n if (!win) {\r\n throw new Error('No window found');\r\n }\r\n\r\n const fullRemotePath = remotePath ? `${remotePath}\\\\${fileName}` : fileName;\r\n let url = `http://${serverHost}:${port}/api/files/${encodeURIComponent(fullRemotePath)}`;\r\n if (password) {\r\n url += `?password=${encodeURIComponent(password)}`;\r\n }\r\n\r\n const response = await fetch(url);\r\n if (!response.ok) {\r\n throw new Error(`Download failed: ${response.statusText}`);\r\n }\r\n\r\n const contentLength = response.headers.get('Content-Length');\r\n if (!contentLength) {\r\n throw new Error('Server did not return Content-Length');\r\n }\r\n\r\n const totalSize = parseInt(contentLength, 10);\r\n const targetDir = localPath || 'C:\\\\';\r\n const targetPath = path.join(targetDir, fileName);\r\n\r\n if (!fs.existsSync(targetDir)) {\r\n fs.mkdirSync(targetDir, { recursive: true });\r\n }\r\n\r\n const fileStream = fs.createWriteStream(targetPath);\r\n const reader = response.body?.getReader();\r\n if (!reader) {\r\n throw new Error('Failed to get response body reader');\r\n }\r\n\r\n let downloadedSize = 0;\r\n const CHUNK_SIZE = 64 * 1024;\r\n\r\n while (true) {\r\n const { done, value } = await reader.read();\r\n if (done) {\r\n break;\r\n }\r\n\r\n if (value) {\r\n downloadedSize += value.length;\r\n fileStream.write(value);\r\n\r\n const progress = Math.round((downloadedSize / totalSize) * 100);\r\n win.webContents.send('download-progress', { id, progress });\r\n }\r\n }\r\n\r\n fileStream.end();\r\n return { success: true, filePath: targetPath };\r\n } catch (error: any) {\r\n log.error('Remote download failed:', error);\r\n return { success: false, error: error.message };\r\n }\r\n});\r\n\r\nasync function startServer() {\r\n if (electronState.isDevelopment()) {\r\n log.info('In dev mode, assuming external servers are running.');\r\n return;\r\n }\r\n\r\n const serverPath = path.join(__dirname, '../dist-api/server.js');\r\n const serverUrl = pathToFileURL(serverPath).href;\r\n\r\n log.info(`Starting internal server from: ${serverPath}`);\r\n try {\r\n const serverModule = await import(serverUrl);\r\n if (serverModule.startServer) {\r\n const port = await serverModule.startServer();\r\n electronState.setServerPort(port);\r\n log.info(`Internal server started successfully on port ${port}`);\r\n } else {\r\n log.warn('startServer function not found in server module, using default port 3001');\r\n }\r\n } catch (e) {\r\n log.error('Failed to start internal server:', e);\r\n }\r\n}\r\n\r\napp.whenReady().then(async () => {\r\n await startServer();\r\n await createWindow();\r\n\r\n startClipboardWatcher();\r\n\r\n globalShortcut.register('CommandOrControl+Shift+C', () => {\r\n log.info('Global shortcut: sync clipboard to remote');\r\n const win = electronState.getMainWindow();\r\n if (win) {\r\n win.webContents.send('remote-clipboard-sync-to-remote');\r\n }\r\n });\r\n\r\n globalShortcut.register('CommandOrControl+Shift+V', () => {\r\n log.info('Global shortcut: sync clipboard from remote');\r\n const win = electronState.getMainWindow();\r\n if (win) {\r\n win.webContents.send('remote-clipboard-sync-from-remote');\r\n }\r\n });\r\n\r\n app.on('activate', () => {\r\n if (BrowserWindow.getAllWindows().length === 0) {\r\n createWindow();\r\n }\r\n });\r\n});\r\n\r\napp.on('window-all-closed', () => {\r\n globalShortcut.unregisterAll();\r\n if (process.platform !== 'darwin') {\r\n app.quit();\r\n }\r\n});\r\n","import { BrowserWindow } from 'electron';\n\n/**\n * 生成 PDF 的服务\n * @param htmlContent 完整的 HTML 字符串\n * @returns PDF 文件的二进制数据\n */\nexport async function generatePdf(htmlContent: string): Promise {\n const printWin = new BrowserWindow({\n show: false,\n webPreferences: {\n nodeIntegration: false,\n contextIsolation: true,\n sandbox: false, // 与 main.ts 保持一致,确保脚本执行权限\n }\n });\n\n try {\n await printWin.loadURL(`data:text/html;charset=utf-8,${encodeURIComponent(htmlContent)}`);\n \n // 等待资源加载完成 (由 generatePrintHtml 注入的脚本控制)\n await printWin.webContents.executeJavaScript(`\n new Promise(resolve => {\n const check = () => {\n if (window.__PRINT_READY__) {\n resolve();\n } else {\n setTimeout(check, 100);\n }\n }\n check();\n })\n `);\n\n const pdfData = await printWin.webContents.printToPDF({\n printBackground: true,\n pageSize: 'A4',\n margins: { top: 0, bottom: 0, left: 0, right: 0 }\n });\n\n return pdfData;\n } finally {\n // 确保窗口被关闭,防止内存泄漏\n printWin.close();\n }\n}\n","import { dialog, BrowserWindow } from 'electron'\nimport path from 'path'\nimport fs from 'fs'\nimport log from 'electron-log'\n\nexport interface HtmlImportResult {\n success: boolean\n canceled?: boolean\n error?: string\n htmlPath?: string\n htmlDir?: string\n htmlFileName?: string\n assetsDirName?: string\n assetsFiles?: string[]\n}\n\nexport const selectHtmlFile = async (win: BrowserWindow | null): Promise => {\n if (!win) return { success: false, error: 'No window found' }\n\n try {\n const { filePaths, canceled } = await dialog.showOpenDialog(win, {\n title: '选择 HTML 文件',\n filters: [\n { name: 'HTML Files', extensions: ['html', 'htm'] }\n ],\n properties: ['openFile']\n })\n\n if (canceled || filePaths.length === 0) {\n return { success: false, canceled: true }\n }\n\n const htmlPath = filePaths[0]\n const htmlDir = path.dirname(htmlPath)\n const htmlFileName = path.basename(htmlPath, path.extname(htmlPath))\n \n const assetsDirName = `${htmlFileName}_files`\n const assetsDirPath = path.join(htmlDir, assetsDirName)\n \n const assetsFiles: string[] = []\n if (fs.existsSync(assetsDirPath)) {\n const collectFiles = (dir: string, baseDir: string) => {\n const entries = fs.readdirSync(dir, { withFileTypes: true })\n for (const entry of entries) {\n const fullPath = path.join(dir, entry.name)\n if (entry.isDirectory()) {\n collectFiles(fullPath, baseDir)\n } else {\n const relPath = path.relative(baseDir, fullPath)\n assetsFiles.push(relPath)\n }\n }\n }\n collectFiles(assetsDirPath, assetsDirPath)\n }\n\n return { \n success: true, \n htmlPath,\n htmlDir,\n htmlFileName,\n assetsDirName,\n assetsFiles\n }\n } catch (error: any) {\n log.error('Select HTML file failed:', error)\n return { success: false, error: error.message }\n }\n}\n","import { BrowserWindow } from 'electron'\n\ninterface ElectronAppState {\n mainWindow: BrowserWindow | null\n serverPort: number\n isDev: boolean\n}\n\nclass ElectronState {\n private state: ElectronAppState = {\n mainWindow: null,\n serverPort: 3001,\n isDev: false,\n }\n\n getMainWindow(): BrowserWindow | null {\n return this.state.mainWindow\n }\n\n setMainWindow(window: BrowserWindow | null): void {\n this.state.mainWindow = window\n }\n\n getServerPort(): number {\n return this.state.serverPort\n }\n\n setServerPort(port: number): void {\n this.state.serverPort = port\n }\n\n isDevelopment(): boolean {\n return this.state.isDev\n }\n\n setDevelopment(isDev: boolean): void {\n this.state.isDev = isDev\n }\n\n reset(): void {\n this.state = {\n mainWindow: null,\n serverPort: 3001,\n isDev: false,\n }\n }\n}\n\nexport const electronState = new ElectronState()\n"],"mappings":"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","names":["BrowserWindow","dialog","path","fs","log","log","path","fs","BrowserWindow","dialog"]} \ No newline at end of file +{"version":3,"sources":["../electron/main.ts","../electron/services/pdfGenerator.ts","../electron/services/htmlImport.ts","../electron/state.ts"],"sourcesContent":["import { app, BrowserWindow, shell, ipcMain, dialog, nativeTheme, globalShortcut, clipboard } from 'electron';\r\nimport path from 'path';\r\nimport { fileURLToPath, pathToFileURL } from 'url';\r\nimport fs from 'fs';\r\nimport log from 'electron-log';\r\nimport { generatePdf } from './services/pdfGenerator';\r\nimport { selectHtmlFile } from './services/htmlImport';\r\nimport { electronState } from './state';\r\n\r\nlog.initialize();\r\n\r\nconst __filename = fileURLToPath(import.meta.url);\r\nconst __dirname = path.dirname(__filename);\r\n\r\nprocess.env.NOTEBOOK_ROOT = path.join(app.getPath('documents'), 'XCDesktop');\r\n\r\nif (!fs.existsSync(process.env.NOTEBOOK_ROOT)) {\r\n try {\r\n fs.mkdirSync(process.env.NOTEBOOK_ROOT, { recursive: true });\r\n } catch (err) {\r\n log.error('Failed to create notebook directory:', err);\r\n }\r\n}\r\n\r\nelectronState.setDevelopment(!app.isPackaged);\r\n\r\nlet lastClipboardText = '';\r\n\r\nfunction startClipboardWatcher() {\r\n lastClipboardText = clipboard.readText();\r\n \r\n setInterval(() => {\r\n try {\r\n const currentText = clipboard.readText();\r\n if (currentText && currentText !== lastClipboardText) {\r\n lastClipboardText = currentText;\r\n log.info('Clipboard changed, syncing to remote');\r\n const win = electronState.getMainWindow();\r\n if (win) {\r\n win.webContents.send('remote-clipboard-auto-sync', currentText);\r\n }\r\n }\r\n } catch (e) {\r\n // ignore\r\n }\r\n }, 1000);\r\n}\r\n\r\nasync function createWindow() {\r\n const initialSymbolColor = nativeTheme.shouldUseDarkColors ? '#ffffff' : '#000000';\r\n \r\n const mainWindow = new BrowserWindow({\r\n width: 1280,\r\n height: 800,\r\n minWidth: 1600,\r\n minHeight: 900,\r\n autoHideMenuBar: true,\r\n titleBarStyle: 'hidden',\r\n titleBarOverlay: {\r\n color: '#00000000',\r\n symbolColor: initialSymbolColor,\r\n height: 32,\r\n },\r\n webPreferences: {\r\n nodeIntegration: false,\r\n contextIsolation: true,\r\n sandbox: false,\r\n webviewTag: true,\r\n preload: path.join(__dirname, 'preload.cjs'),\r\n },\r\n });\r\n\r\n electronState.setMainWindow(mainWindow);\r\n mainWindow.setMenu(null);\r\n\r\n mainWindow.webContents.setWindowOpenHandler(({ url }) => {\r\n if (url.startsWith('http:') || url.startsWith('https:')) {\r\n shell.openExternal(url);\r\n return { action: 'deny' };\r\n }\r\n return { action: 'allow' };\r\n });\r\n\r\n if (electronState.isDevelopment()) {\r\n log.info('Loading development URL...');\r\n try {\r\n await mainWindow.loadURL('http://localhost:5173');\r\n } catch (e) {\r\n log.error('Failed to load dev URL. Make sure npm run electron:dev is used.', e);\r\n }\r\n mainWindow.webContents.openDevTools();\r\n } else {\r\n log.info(`Loading production URL with port ${electronState.getServerPort()}...`);\r\n await mainWindow.loadURL(`http://localhost:${electronState.getServerPort()}`);\r\n }\r\n}\r\n\r\nipcMain.handle('export-pdf', async (event, title, htmlContent) => {\r\n const win = BrowserWindow.fromWebContents(event.sender);\r\n if (!win) return { success: false, error: 'No window found' };\r\n\r\n try {\r\n const { filePath } = await dialog.showSaveDialog(win, {\r\n title: '导出 PDF',\r\n defaultPath: `${title}.pdf`,\r\n filters: [{ name: 'PDF Files', extensions: ['pdf'] }]\r\n });\r\n\r\n if (!filePath) return { success: false, canceled: true };\r\n\r\n if (!htmlContent) {\r\n throw new Error('No HTML content provided for PDF export');\r\n }\r\n\r\n const pdfData = await generatePdf(htmlContent);\r\n fs.writeFileSync(filePath, pdfData);\r\n\r\n return { success: true, filePath };\r\n } catch (error: any) {\r\n log.error('Export PDF failed:', error);\r\n return { success: false, error: error.message };\r\n }\r\n});\r\n\r\nipcMain.handle('select-html-file', async (event) => {\r\n const win = BrowserWindow.fromWebContents(event.sender);\r\n return selectHtmlFile(win);\r\n});\r\n\r\nipcMain.handle('update-titlebar-buttons', async (event, symbolColor: string) => {\r\n const win = BrowserWindow.fromWebContents(event.sender);\r\n if (win) {\r\n win.setTitleBarOverlay({ symbolColor });\r\n return { success: true };\r\n }\r\n return { success: false };\r\n});\r\n\r\nipcMain.handle('clipboard-read-text', async () => {\r\n try {\r\n const text = clipboard.readText();\r\n return { success: true, text };\r\n } catch (error: any) {\r\n log.error('Clipboard read failed:', error);\r\n return { success: false, error: error.message };\r\n }\r\n});\r\n\r\nipcMain.handle('clipboard-write-text', async (event, text: string) => {\r\n try {\r\n clipboard.writeText(text);\r\n return { success: true };\r\n } catch (error: any) {\r\n log.error('Clipboard write failed:', error);\r\n return { success: false, error: error.message };\r\n }\r\n});\r\n\r\nipcMain.handle('remote-fetch-drives', async (_event, serverHost: string, port: number, password?: string) => {\r\n try {\r\n let url = `http://${serverHost}:${port}/api/files/drives`;\r\n if (password) {\r\n url += `?password=${encodeURIComponent(password)}`;\r\n }\r\n const response = await fetch(url);\r\n if (!response.ok) {\r\n throw new Error(`Failed to fetch drives: ${response.statusText}`);\r\n }\r\n const data = await response.json();\r\n const items = data.items || [];\r\n return {\r\n success: true,\r\n data: items.map((item: { name: string; isDirectory: boolean; size: number }) => ({\r\n name: item.name,\r\n path: item.name,\r\n type: item.isDirectory ? 'dir' : 'file',\r\n size: item.size,\r\n modified: '',\r\n }))\r\n };\r\n } catch (error: any) {\r\n log.error('Remote fetch drives failed:', error);\r\n return { success: false, error: error.message };\r\n }\r\n});\r\n\r\nipcMain.handle('remote-fetch-files', async (_event, serverHost: string, port: number, filePath: string, password?: string) => {\r\n try {\r\n let url = `http://${serverHost}:${port}/api/files/browse?path=${encodeURIComponent(filePath)}&allowSystem=true`;\r\n if (password) {\r\n url += `&password=${encodeURIComponent(password)}`;\r\n }\r\n const response = await fetch(url);\r\n if (!response.ok) {\r\n throw new Error(`Failed to fetch files: ${response.statusText}`);\r\n }\r\n const data = await response.json();\r\n const items = data.items || [];\r\n return {\r\n success: true,\r\n data: items.map((item: { name: string; isDirectory: boolean; size: number; modified: Date }) => ({\r\n name: item.name,\r\n path: data.currentPath ? `${data.currentPath}/${item.name}` : item.name,\r\n type: item.isDirectory ? 'dir' : 'file',\r\n size: item.size,\r\n modified: item.modified?.toString(),\r\n }))\r\n };\r\n } catch (error: any) {\r\n log.error('Remote fetch files failed:', error);\r\n return { success: false, error: error.message };\r\n }\r\n});\r\n\r\nipcMain.handle('remote-upload-file', async (_event, id: string, serverHost: string, port: number, filePath: string, remotePath: string, password?: string) => {\r\n try {\r\n const win = electronState.getMainWindow();\r\n if (!win) {\r\n throw new Error('No window found');\r\n }\r\n\r\n const fullPath = path.resolve(filePath);\r\n if (!fs.existsSync(fullPath)) {\r\n throw new Error('File not found');\r\n }\r\n\r\n const stats = fs.statSync(fullPath);\r\n const fileSize = stats.size;\r\n const fileName = path.basename(fullPath);\r\n\r\n let url = `http://${serverHost}:${port}/api/files/upload/start`;\r\n if (password) {\r\n url += `?password=${encodeURIComponent(password)}`;\r\n }\r\n\r\n const startResponse = await fetch(url, {\r\n method: 'POST',\r\n headers: { 'Content-Type': 'application/json' },\r\n body: JSON.stringify({ filename: fileName, fileSize }),\r\n });\r\n\r\n if (!startResponse.ok) {\r\n throw new Error(`Failed to start upload: ${startResponse.statusText}`);\r\n }\r\n\r\n const { fileId, chunkSize } = await startResponse.json();\r\n const CHUNK_SIZE = chunkSize || (64 * 1024);\r\n const totalChunks = Math.ceil(fileSize / CHUNK_SIZE);\r\n\r\n const readStream = fs.createReadStream(fullPath, { highWaterMark: CHUNK_SIZE });\r\n let chunkIndex = 0;\r\n let uploadedBytes = 0;\r\n\r\n for await (const chunk of readStream) {\r\n const formData = new FormData();\r\n const blob = new Blob([chunk]);\r\n formData.append('chunk', blob, fileName);\r\n formData.append('fileId', fileId);\r\n formData.append('chunkIndex', chunkIndex.toString());\r\n\r\n const chunkUrl = `http://${serverHost}:${port}/api/files/upload/chunk${password ? `?password=${encodeURIComponent(password)}` : ''}`;\r\n const chunkResponse = await fetch(chunkUrl, {\r\n method: 'POST',\r\n body: formData,\r\n });\r\n\r\n if (!chunkResponse.ok) {\r\n throw new Error(`Failed to upload chunk ${chunkIndex}: ${chunkResponse.statusText}`);\r\n }\r\n\r\n uploadedBytes += chunk.length;\r\n const progress = Math.round((uploadedBytes / fileSize) * 100);\r\n win.webContents.send('upload-progress', { id, progress });\r\n\r\n chunkIndex++;\r\n }\r\n\r\n const mergeUrl = `http://${serverHost}:${port}/api/files/upload/merge${password ? `?password=${encodeURIComponent(password)}` : ''}`;\r\n const mergeResponse = await fetch(mergeUrl, {\r\n method: 'POST',\r\n headers: { 'Content-Type': 'application/json' },\r\n body: JSON.stringify({ fileId, totalChunks, filename: fileName, path: remotePath }),\r\n });\r\n\r\n if (!mergeResponse.ok) {\r\n throw new Error(`Failed to merge chunks: ${mergeResponse.statusText}`);\r\n }\r\n\r\n return { success: true };\r\n } catch (error: any) {\r\n log.error('Remote upload failed:', error);\r\n return { success: false, error: error.message };\r\n }\r\n});\r\n\r\nipcMain.handle('remote-download-file', async (_event, id: string, serverHost: string, port: number, fileName: string, remotePath: string, localPath: string, password?: string) => {\r\n try {\r\n log.info('Remote download params:', { id, serverHost, port, fileName, remotePath, localPath, password });\r\n \r\n const win = electronState.getMainWindow();\r\n if (!win) {\r\n throw new Error('No window found');\r\n }\r\n\r\n const fullRemotePath = remotePath ? `${remotePath}\\\\${fileName}` : fileName;\r\n let url = `http://${serverHost}:${port}/api/files/${encodeURIComponent(fullRemotePath)}`;\r\n if (password) {\r\n url += `?password=${encodeURIComponent(password)}`;\r\n }\r\n\r\n const response = await fetch(url);\r\n if (!response.ok) {\r\n throw new Error(`Download failed: ${response.statusText}`);\r\n }\r\n\r\n const contentLength = response.headers.get('Content-Length');\r\n if (!contentLength) {\r\n throw new Error('Server did not return Content-Length');\r\n }\r\n\r\n const totalSize = parseInt(contentLength, 10);\r\n const targetDir = localPath || 'C:\\\\';\r\n const targetPath = path.join(targetDir, fileName);\r\n\r\n if (!fs.existsSync(targetDir)) {\r\n fs.mkdirSync(targetDir, { recursive: true });\r\n }\r\n\r\n const fileStream = fs.createWriteStream(targetPath);\r\n const reader = response.body?.getReader();\r\n if (!reader) {\r\n throw new Error('Failed to get response body reader');\r\n }\r\n\r\n let downloadedSize = 0;\r\n const CHUNK_SIZE = 64 * 1024;\r\n\r\n while (true) {\r\n const { done, value } = await reader.read();\r\n if (done) {\r\n break;\r\n }\r\n\r\n if (value) {\r\n downloadedSize += value.length;\r\n fileStream.write(value);\r\n\r\n const progress = Math.round((downloadedSize / totalSize) * 100);\r\n win.webContents.send('download-progress', { id, progress });\r\n }\r\n }\r\n\r\n fileStream.end();\r\n return { success: true, filePath: targetPath };\r\n } catch (error: any) {\r\n log.error('Remote download failed:', error);\r\n return { success: false, error: error.message };\r\n }\r\n});\r\n\r\nasync function startServer() {\r\n if (electronState.isDevelopment()) {\r\n log.info('In dev mode, assuming external servers are running.');\r\n return;\r\n }\r\n\r\n const serverPath = path.join(__dirname, '../dist-api/server.js');\r\n const serverUrl = pathToFileURL(serverPath).href;\r\n\r\n log.info(`Starting internal server from: ${serverPath}`);\r\n try {\r\n const serverModule = await import(serverUrl);\r\n if (serverModule.startServer) {\r\n const port = await serverModule.startServer();\r\n electronState.setServerPort(port);\r\n log.info(`Internal server started successfully on port ${port}`);\r\n } else {\r\n log.warn('startServer function not found in server module, using default port 3001');\r\n }\r\n } catch (e) {\r\n log.error('Failed to start internal server:', e);\r\n }\r\n}\r\n\r\napp.whenReady().then(async () => {\r\n await startServer();\r\n await createWindow();\r\n\r\n startClipboardWatcher();\r\n\r\n globalShortcut.register('CommandOrControl+Shift+C', () => {\r\n log.info('Global shortcut: sync clipboard to remote');\r\n const win = electronState.getMainWindow();\r\n if (win) {\r\n win.webContents.send('remote-clipboard-sync-to-remote');\r\n }\r\n });\r\n\r\n globalShortcut.register('CommandOrControl+Shift+V', () => {\r\n log.info('Global shortcut: sync clipboard from remote');\r\n const win = electronState.getMainWindow();\r\n if (win) {\r\n win.webContents.send('remote-clipboard-sync-from-remote');\r\n }\r\n });\r\n\r\n app.on('activate', () => {\r\n if (BrowserWindow.getAllWindows().length === 0) {\r\n createWindow();\r\n }\r\n });\r\n});\r\n\r\napp.on('window-all-closed', () => {\r\n globalShortcut.unregisterAll();\r\n if (process.platform !== 'darwin') {\r\n app.quit();\r\n }\r\n});\r\n","import { BrowserWindow } from 'electron';\n\n/**\n * 生成 PDF 的服务\n * @param htmlContent 完整的 HTML 字符串\n * @returns PDF 文件的二进制数据\n */\nexport async function generatePdf(htmlContent: string): Promise {\n const printWin = new BrowserWindow({\n show: false,\n webPreferences: {\n nodeIntegration: false,\n contextIsolation: true,\n sandbox: false, // 与 main.ts 保持一致,确保脚本执行权限\n }\n });\n\n try {\n await printWin.loadURL(`data:text/html;charset=utf-8,${encodeURIComponent(htmlContent)}`);\n \n // 等待资源加载完成 (由 generatePrintHtml 注入的脚本控制)\n await printWin.webContents.executeJavaScript(`\n new Promise(resolve => {\n const check = () => {\n if (window.__PRINT_READY__) {\n resolve();\n } else {\n setTimeout(check, 100);\n }\n }\n check();\n })\n `);\n\n const pdfData = await printWin.webContents.printToPDF({\n printBackground: true,\n pageSize: 'A4',\n margins: { top: 0, bottom: 0, left: 0, right: 0 }\n });\n\n return pdfData;\n } finally {\n // 确保窗口被关闭,防止内存泄漏\n printWin.close();\n }\n}\n","import { dialog, BrowserWindow } from 'electron'\nimport path from 'path'\nimport fs from 'fs'\nimport log from 'electron-log'\n\nexport interface HtmlImportResult {\n success: boolean\n canceled?: boolean\n error?: string\n htmlPath?: string\n htmlDir?: string\n htmlFileName?: string\n assetsDirName?: string\n assetsFiles?: string[]\n}\n\nexport const selectHtmlFile = async (win: BrowserWindow | null): Promise => {\n if (!win) return { success: false, error: 'No window found' }\n\n try {\n const { filePaths, canceled } = await dialog.showOpenDialog(win, {\n title: '选择 HTML 文件',\n filters: [\n { name: 'HTML Files', extensions: ['html', 'htm'] }\n ],\n properties: ['openFile']\n })\n\n if (canceled || filePaths.length === 0) {\n return { success: false, canceled: true }\n }\n\n const htmlPath = filePaths[0]\n const htmlDir = path.dirname(htmlPath)\n const htmlFileName = path.basename(htmlPath, path.extname(htmlPath))\n \n const assetsDirName = `${htmlFileName}_files`\n const assetsDirPath = path.join(htmlDir, assetsDirName)\n \n const assetsFiles: string[] = []\n if (fs.existsSync(assetsDirPath)) {\n const collectFiles = (dir: string, baseDir: string) => {\n const entries = fs.readdirSync(dir, { withFileTypes: true })\n for (const entry of entries) {\n const fullPath = path.join(dir, entry.name)\n if (entry.isDirectory()) {\n collectFiles(fullPath, baseDir)\n } else {\n const relPath = path.relative(baseDir, fullPath)\n assetsFiles.push(relPath)\n }\n }\n }\n collectFiles(assetsDirPath, assetsDirPath)\n }\n\n return { \n success: true, \n htmlPath,\n htmlDir,\n htmlFileName,\n assetsDirName,\n assetsFiles\n }\n } catch (error: any) {\n log.error('Select HTML file failed:', error)\n return { success: false, error: error.message }\n }\n}\n","import { BrowserWindow } from 'electron'\n\ninterface ElectronAppState {\n mainWindow: BrowserWindow | null\n serverPort: number\n isDev: boolean\n}\n\nclass ElectronState {\n private state: ElectronAppState = {\n mainWindow: null,\n serverPort: 3001,\n isDev: false,\n }\n\n getMainWindow(): BrowserWindow | null {\n return this.state.mainWindow\n }\n\n setMainWindow(window: BrowserWindow | null): void {\n this.state.mainWindow = window\n }\n\n getServerPort(): number {\n return this.state.serverPort\n }\n\n setServerPort(port: number): void {\n this.state.serverPort = port\n }\n\n isDevelopment(): boolean {\n return this.state.isDev\n }\n\n setDevelopment(isDev: boolean): void {\n this.state.isDev = isDev\n }\n\n reset(): void {\n this.state = {\n mainWindow: null,\n serverPort: 3001,\n isDev: false,\n }\n }\n}\n\nexport const electronState = new ElectronState()\n"],"mappings":"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","names":["BrowserWindow","dialog","path","fs","log","log","path","fs","BrowserWindow","dialog"]} \ No newline at end of file diff --git a/dist-electron/preload.cjs b/dist-electron/preload.cjs index 86f69ac..544f833 100644 --- a/dist-electron/preload.cjs +++ b/dist-electron/preload.cjs @@ -23,11 +23,16 @@ import_electron.contextBridge.exposeInMainWorld("electronAPI", { import_electron.ipcRenderer.on("download-progress", handler); return () => import_electron.ipcRenderer.removeListener("download-progress", handler); }, + onUploadProgress: (callback) => { + const handler = (_event, data) => callback(data); + import_electron.ipcRenderer.on("upload-progress", handler); + return () => import_electron.ipcRenderer.removeListener("upload-progress", handler); + }, clipboardReadText: () => import_electron.ipcRenderer.invoke("clipboard-read-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), + remoteUploadFile: (id, serverHost, port, filePath, remotePath, password) => import_electron.ipcRenderer.invoke("remote-upload-file", id, serverHost, port, filePath, remotePath, password), remoteDownloadFile: (id, serverHost, port, fileName, remotePath, localPath, password) => import_electron.ipcRenderer.invoke("remote-download-file", id, serverHost, port, fileName, remotePath, localPath, password) }); //# sourceMappingURL=preload.cjs.map \ No newline at end of file diff --git a/dist-electron/preload.cjs.map b/dist-electron/preload.cjs.map index 2a8cf1a..6d22fe2 100644 --- a/dist-electron/preload.cjs.map +++ b/dist-electron/preload.cjs.map @@ -1 +1 @@ -{"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 onDownloadProgress: (callback: (data: { progress: number; id: string }) => void) => {\r\n const handler = (_event: Electron.IpcRendererEvent, data: { progress: number; id: string }) => callback(data);\r\n ipcRenderer.on('download-progress', handler);\r\n return () => ipcRenderer.removeListener('download-progress', 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: (id: string, serverHost: string, port: number, fileName: string, remotePath: string, localPath: string, password?: string) => \r\n ipcRenderer.invoke('remote-download-file', id, serverHost, port, fileName, remotePath, localPath, 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,oBAAoB,CAAC,aAA+D;AAClF,UAAM,UAAU,CAAC,QAAmC,SAA2C,SAAS,IAAI;AAC5G,gCAAY,GAAG,qBAAqB,OAAO;AAC3C,WAAO,MAAM,4BAAY,eAAe,qBAAqB,OAAO;AAAA,EACtE;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,IAAY,YAAoB,MAAc,UAAkB,YAAoB,WAAmB,aAC1H,4BAAY,OAAO,wBAAwB,IAAI,YAAY,MAAM,UAAU,YAAY,WAAW,QAAQ;AAC9G,CAAC;","names":[]} \ No newline at end of file +{"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 onDownloadProgress: (callback: (data: { progress: number; id: string }) => void) => {\r\n const handler = (_event: Electron.IpcRendererEvent, data: { progress: number; id: string }) => callback(data);\r\n ipcRenderer.on('download-progress', handler);\r\n return () => ipcRenderer.removeListener('download-progress', handler);\r\n },\r\n onUploadProgress: (callback: (data: { progress: number; id: string }) => void) => {\r\n const handler = (_event: Electron.IpcRendererEvent, data: { progress: number; id: string }) => callback(data);\r\n ipcRenderer.on('upload-progress', handler);\r\n return () => ipcRenderer.removeListener('upload-progress', 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: (id: string, serverHost: string, port: number, filePath: string, remotePath: string, password?: string) => \r\n ipcRenderer.invoke('remote-upload-file', id, serverHost, port, filePath, remotePath, password),\r\n remoteDownloadFile: (id: string, serverHost: string, port: number, fileName: string, remotePath: string, localPath: string, password?: string) => \r\n ipcRenderer.invoke('remote-download-file', id, serverHost, port, fileName, remotePath, localPath, 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,oBAAoB,CAAC,aAA+D;AAClF,UAAM,UAAU,CAAC,QAAmC,SAA2C,SAAS,IAAI;AAC5G,gCAAY,GAAG,qBAAqB,OAAO;AAC3C,WAAO,MAAM,4BAAY,eAAe,qBAAqB,OAAO;AAAA,EACtE;AAAA,EACA,kBAAkB,CAAC,aAA+D;AAChF,UAAM,UAAU,CAAC,QAAmC,SAA2C,SAAS,IAAI;AAC5G,gCAAY,GAAG,mBAAmB,OAAO;AACzC,WAAO,MAAM,4BAAY,eAAe,mBAAmB,OAAO;AAAA,EACpE;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,IAAY,YAAoB,MAAc,UAAkB,YAAoB,aACrG,4BAAY,OAAO,sBAAsB,IAAI,YAAY,MAAM,UAAU,YAAY,QAAQ;AAAA,EAC/F,oBAAoB,CAAC,IAAY,YAAoB,MAAc,UAAkB,YAAoB,WAAmB,aAC1H,4BAAY,OAAO,wBAAwB,IAAI,YAAY,MAAM,UAAU,YAAY,WAAW,QAAQ;AAC9G,CAAC;","names":[]} \ No newline at end of file diff --git a/electron/main.ts b/electron/main.ts index 05a27b2..28f4fbe 100644 --- a/electron/main.ts +++ b/electron/main.ts @@ -212,7 +212,7 @@ ipcMain.handle('remote-fetch-files', async (_event, serverHost: string, port: nu } }); -ipcMain.handle('remote-upload-file', async (_event, serverHost: string, port: number, filePath: string, remotePath: string, password?: string) => { +ipcMain.handle('remote-upload-file', async (_event, id: string, serverHost: string, port: number, filePath: string, remotePath: string, password?: string) => { try { const win = electronState.getMainWindow(); if (!win) { @@ -224,28 +224,66 @@ ipcMain.handle('remote-upload-file', async (_event, serverHost: string, port: nu throw new Error('File not found'); } - const fileBuffer = fs.readFileSync(fullPath); + const stats = fs.statSync(fullPath); + const fileSize = stats.size; const fileName = path.basename(fullPath); - let url = `http://${serverHost}:${port}/api/files/upload`; + let url = `http://${serverHost}:${port}/api/files/upload/start`; 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, { + const startResponse = await fetch(url, { method: 'POST', - body: formData, + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ filename: fileName, fileSize }), }); - if (!response.ok) { - throw new Error(`Upload failed: ${response.statusText}`); + if (!startResponse.ok) { + throw new Error(`Failed to start upload: ${startResponse.statusText}`); + } + + const { fileId, chunkSize } = await startResponse.json(); + const CHUNK_SIZE = chunkSize || (64 * 1024); + const totalChunks = Math.ceil(fileSize / CHUNK_SIZE); + + const readStream = fs.createReadStream(fullPath, { highWaterMark: CHUNK_SIZE }); + let chunkIndex = 0; + let uploadedBytes = 0; + + for await (const chunk of readStream) { + const formData = new FormData(); + const blob = new Blob([chunk]); + formData.append('chunk', blob, fileName); + formData.append('fileId', fileId); + formData.append('chunkIndex', chunkIndex.toString()); + + const chunkUrl = `http://${serverHost}:${port}/api/files/upload/chunk${password ? `?password=${encodeURIComponent(password)}` : ''}`; + const chunkResponse = await fetch(chunkUrl, { + method: 'POST', + body: formData, + }); + + if (!chunkResponse.ok) { + throw new Error(`Failed to upload chunk ${chunkIndex}: ${chunkResponse.statusText}`); + } + + uploadedBytes += chunk.length; + const progress = Math.round((uploadedBytes / fileSize) * 100); + win.webContents.send('upload-progress', { id, progress }); + + chunkIndex++; + } + + const mergeUrl = `http://${serverHost}:${port}/api/files/upload/merge${password ? `?password=${encodeURIComponent(password)}` : ''}`; + const mergeResponse = await fetch(mergeUrl, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ fileId, totalChunks, filename: fileName, path: remotePath }), + }); + + if (!mergeResponse.ok) { + throw new Error(`Failed to merge chunks: ${mergeResponse.statusText}`); } return { success: true }; diff --git a/electron/preload.ts b/electron/preload.ts index b46b079..cae8b8a 100644 --- a/electron/preload.ts +++ b/electron/preload.ts @@ -24,14 +24,19 @@ contextBridge.exposeInMainWorld('electronAPI', { ipcRenderer.on('download-progress', handler); return () => ipcRenderer.removeListener('download-progress', handler); }, + onUploadProgress: (callback: (data: { progress: number; id: string }) => void) => { + const handler = (_event: Electron.IpcRendererEvent, data: { progress: number; id: string }) => callback(data); + ipcRenderer.on('upload-progress', handler); + return () => ipcRenderer.removeListener('upload-progress', handler); + }, 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), + remoteUploadFile: (id: string, serverHost: string, port: number, filePath: string, remotePath: string, password?: string) => + ipcRenderer.invoke('remote-upload-file', id, serverHost, port, filePath, remotePath, password), remoteDownloadFile: (id: string, serverHost: string, port: number, fileName: string, remotePath: string, localPath: string, password?: string) => ipcRenderer.invoke('remote-download-file', id, serverHost, port, fileName, remotePath, localPath, password), }) diff --git a/remote/src/routes/files.js b/remote/src/routes/files.js index c477ff6..913f137 100644 --- a/remote/src/routes/files.js +++ b/remote/src/routes/files.js @@ -157,13 +157,13 @@ router.post('/upload/chunk', upload.single('chunk'), (req, res) => { router.post('/upload/merge', (req, res) => { try { - const { fileId, totalChunks, filename } = req.body; + const { fileId, totalChunks, filename, path: targetPath } = req.body; if (!fileId || !totalChunks || !filename) { return res.status(400).json({ error: 'Missing required fields' }); } - const success = fileService.mergeChunks(fileId, parseInt(totalChunks), filename); + const success = fileService.mergeChunks(fileId, parseInt(totalChunks), filename, targetPath); if (success) { res.json({ success: true, filename }); diff --git a/remote/src/services/file/FileService.js b/remote/src/services/file/FileService.js index 2e9100d..9274ec3 100644 --- a/remote/src/services/file/FileService.js +++ b/remote/src/services/file/FileService.js @@ -91,9 +91,13 @@ class FileService { } } - mergeChunks(fileId, totalChunks, filename) { + mergeChunks(fileId, totalChunks, filename, targetPath) { try { - const filePath = path.normalize(filename); + let targetDir = targetPath || 'C:\\'; + if (!fs.existsSync(targetDir)) { + fs.mkdirSync(targetDir, { recursive: true }); + } + const filePath = path.join(targetDir, filename); const dir = path.dirname(filePath); if (!fs.existsSync(dir)) { diff --git a/src/modules/remote/api.ts b/src/modules/remote/api.ts index 30bb9d8..5e0c1b6 100644 --- a/src/modules/remote/api.ts +++ b/src/modules/remote/api.ts @@ -102,11 +102,28 @@ export const uploadFileToRemote = async ( password?: string, onProgress?: (progress: number) => void ): Promise => { - 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 transferId = Date.now().toString() + + const cleanup = window.electronAPI?.onUploadProgress?.((data) => { + if (data.id === transferId) { + onProgress?.(data.progress) + } + }) + + try { + const result = await window.electronAPI.remoteUploadFile( + transferId, + serverHost, + port, + filePath, + remotePath, + password + ) + if (!result.success) { + throw new Error(result.error || 'Upload failed') + } + } finally { + cleanup?.() } } diff --git a/src/types/electron.d.ts b/src/types/electron.d.ts index 5af888c..a56d92e 100644 --- a/src/types/electron.d.ts +++ b/src/types/electron.d.ts @@ -15,6 +15,7 @@ export interface ElectronAPI { onRemoteClipboardSyncFromRemote: (callback: () => void) => () => void onRemoteClipboardAutoSync: (callback: (text: string) => void) => () => void onDownloadProgress: (callback: (data: { progress: number; id: string }) => void) => () => void + onUploadProgress: (callback: (data: { progress: number; id: 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<{ @@ -27,7 +28,7 @@ export interface ElectronAPI { 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<{ + remoteUploadFile: (id: string, serverHost: string, port: number, filePath: string, remotePath: string, password?: string) => Promise<{ success: boolean error?: string }>