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

@@ -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 };