feat(remote): 实现文件上传真实进度显示

- 使用分块上传替代一次性上传
- 调用 /upload/start → /upload/chunk → /upload/merge 接口
- 通过 IPC 事件实时推送上传进度到前端
- 修复 merge 时未使用目标路径的问题
This commit is contained in:
2026-03-10 15:36:10 +08:00
parent 433db24688
commit de4c101b36
10 changed files with 142 additions and 42 deletions

View File

@@ -316,7 +316,7 @@ ipcMain.handle("remote-fetch-files", async (_event, serverHost, port, filePath,
return { success: false, error: error.message }; 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 { try {
const win = electronState.getMainWindow(); const win = electronState.getMainWindow();
if (!win) { if (!win) {
@@ -326,24 +326,54 @@ ipcMain.handle("remote-upload-file", async (_event, serverHost, port, filePath,
if (!fs2.existsSync(fullPath)) { if (!fs2.existsSync(fullPath)) {
throw new Error("File not found"); 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); const fileName = path2.basename(fullPath);
let url = `http://${serverHost}:${port}/api/files/upload`; let url = `http://${serverHost}:${port}/api/files/upload/start`;
if (password) { if (password) {
url += `?password=${encodeURIComponent(password)}`; url += `?password=${encodeURIComponent(password)}`;
} }
const formData = new FormData(); const startResponse = await fetch(url, {
const blob = new Blob([fileBuffer]);
formData.append("file", blob, fileName);
if (remotePath) {
formData.append("path", remotePath);
}
const response = await fetch(url, {
method: "POST", method: "POST",
body: formData headers: { "Content-Type": "application/json" },
body: JSON.stringify({ filename: fileName, fileSize })
}); });
if (!response.ok) { if (!startResponse.ok) {
throw new Error(`Upload failed: ${response.statusText}`); 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 }; return { success: true };
} catch (error) { } catch (error) {

File diff suppressed because one or more lines are too long

View File

@@ -23,11 +23,16 @@ import_electron.contextBridge.exposeInMainWorld("electronAPI", {
import_electron.ipcRenderer.on("download-progress", handler); import_electron.ipcRenderer.on("download-progress", handler);
return () => import_electron.ipcRenderer.removeListener("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"), 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), 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), 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) 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 //# sourceMappingURL=preload.cjs.map

View File

@@ -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":[]} {"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":[]}

View File

@@ -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 { try {
const win = electronState.getMainWindow(); const win = electronState.getMainWindow();
if (!win) { if (!win) {
@@ -224,28 +224,66 @@ ipcMain.handle('remote-upload-file', async (_event, serverHost: string, port: nu
throw new Error('File not found'); 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); const fileName = path.basename(fullPath);
let url = `http://${serverHost}:${port}/api/files/upload`; let url = `http://${serverHost}:${port}/api/files/upload/start`;
if (password) { if (password) {
url += `?password=${encodeURIComponent(password)}`; url += `?password=${encodeURIComponent(password)}`;
} }
const formData = new FormData(); const startResponse = await fetch(url, {
const blob = new Blob([fileBuffer]);
formData.append('file', blob, fileName);
if (remotePath) {
formData.append('path', remotePath);
}
const response = await fetch(url, {
method: 'POST', method: 'POST',
body: formData, headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ filename: fileName, fileSize }),
}); });
if (!response.ok) { if (!startResponse.ok) {
throw new Error(`Upload failed: ${response.statusText}`); 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 }; return { success: true };

View File

@@ -24,14 +24,19 @@ contextBridge.exposeInMainWorld('electronAPI', {
ipcRenderer.on('download-progress', handler); ipcRenderer.on('download-progress', handler);
return () => ipcRenderer.removeListener('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'), clipboardReadText: () => ipcRenderer.invoke('clipboard-read-text'),
clipboardWriteText: (text: string) => ipcRenderer.invoke('clipboard-write-text', text), clipboardWriteText: (text: string) => ipcRenderer.invoke('clipboard-write-text', text),
remoteFetchDrives: (serverHost: string, port: number, password?: string) => remoteFetchDrives: (serverHost: string, port: number, password?: string) =>
ipcRenderer.invoke('remote-fetch-drives', serverHost, port, password), ipcRenderer.invoke('remote-fetch-drives', serverHost, port, password),
remoteFetchFiles: (serverHost: string, port: number, filePath: string, password?: string) => remoteFetchFiles: (serverHost: string, port: number, filePath: string, password?: string) =>
ipcRenderer.invoke('remote-fetch-files', serverHost, port, filePath, password), ipcRenderer.invoke('remote-fetch-files', serverHost, port, filePath, password),
remoteUploadFile: (serverHost: string, port: number, filePath: string, remotePath: string, password?: string) => remoteUploadFile: (id: string, serverHost: string, port: number, filePath: string, remotePath: string, password?: string) =>
ipcRenderer.invoke('remote-upload-file', serverHost, port, filePath, remotePath, password), 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) => 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), ipcRenderer.invoke('remote-download-file', id, serverHost, port, fileName, remotePath, localPath, password),
}) })

View File

@@ -157,13 +157,13 @@ router.post('/upload/chunk', upload.single('chunk'), (req, res) => {
router.post('/upload/merge', (req, res) => { router.post('/upload/merge', (req, res) => {
try { try {
const { fileId, totalChunks, filename } = req.body; const { fileId, totalChunks, filename, path: targetPath } = req.body;
if (!fileId || !totalChunks || !filename) { if (!fileId || !totalChunks || !filename) {
return res.status(400).json({ error: 'Missing required fields' }); 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) { if (success) {
res.json({ success: true, filename }); res.json({ success: true, filename });

View File

@@ -91,9 +91,13 @@ class FileService {
} }
} }
mergeChunks(fileId, totalChunks, filename) { mergeChunks(fileId, totalChunks, filename, targetPath) {
try { 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); const dir = path.dirname(filePath);
if (!fs.existsSync(dir)) { if (!fs.existsSync(dir)) {

View File

@@ -102,11 +102,28 @@ export const uploadFileToRemote = async (
password?: string, password?: string,
onProgress?: (progress: number) => void onProgress?: (progress: number) => void
): Promise<void> => { ): Promise<void> => {
onProgress?.(50) const transferId = Date.now().toString()
const result = await window.electronAPI.remoteUploadFile(serverHost, port, filePath, remotePath, password)
onProgress?.(100) const cleanup = window.electronAPI?.onUploadProgress?.((data) => {
if (!result.success) { if (data.id === transferId) {
throw new Error(result.error || 'Upload failed') 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?.()
} }
} }

View File

@@ -15,6 +15,7 @@ export interface ElectronAPI {
onRemoteClipboardSyncFromRemote: (callback: () => void) => () => void onRemoteClipboardSyncFromRemote: (callback: () => void) => () => void
onRemoteClipboardAutoSync: (callback: (text: string) => void) => () => void onRemoteClipboardAutoSync: (callback: (text: string) => void) => () => void
onDownloadProgress: (callback: (data: { progress: number; id: 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 }> clipboardReadText: () => Promise<{ success: boolean; text?: string; error?: string }>
clipboardWriteText: (text: string) => Promise<{ success: boolean; error?: string }> clipboardWriteText: (text: string) => Promise<{ success: boolean; error?: string }>
remoteFetchDrives: (serverHost: string, port: number, password?: string) => Promise<{ 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 }> data?: Array<{ name: string; path: string; type: 'file' | 'dir'; size: number; modified?: string }>
error?: 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 success: boolean
error?: string error?: string
}> }>