Files
XCDesktop/dist-electron/main.js
ssdfasd de4c101b36 feat(remote): 实现文件上传真实进度显示
- 使用分块上传替代一次性上传
- 调用 /upload/start → /upload/chunk → /upload/merge 接口
- 通过 IPC 事件实时推送上传进度到前端
- 修复 merge 时未使用目标路径的问题
2026-03-10 15:36:10 +08:00

487 lines
16 KiB
JavaScript

var __defProp = Object.defineProperty;
var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value);
// electron/main.ts
import { app, BrowserWindow as BrowserWindow3, shell, ipcMain, dialog as dialog2, nativeTheme, globalShortcut, clipboard } from "electron";
import path2 from "path";
import { fileURLToPath, pathToFileURL } from "url";
import fs2 from "fs";
import log2 from "electron-log";
// electron/services/pdfGenerator.ts
import { BrowserWindow } from "electron";
async function generatePdf(htmlContent) {
const printWin = new BrowserWindow({
show: false,
webPreferences: {
nodeIntegration: false,
contextIsolation: true,
sandbox: false
// 与 main.ts 保持一致,确保脚本执行权限
}
});
try {
await printWin.loadURL(`data:text/html;charset=utf-8,${encodeURIComponent(htmlContent)}`);
await printWin.webContents.executeJavaScript(`
new Promise(resolve => {
const check = () => {
if (window.__PRINT_READY__) {
resolve();
} else {
setTimeout(check, 100);
}
}
check();
})
`);
const pdfData = await printWin.webContents.printToPDF({
printBackground: true,
pageSize: "A4",
margins: { top: 0, bottom: 0, left: 0, right: 0 }
});
return pdfData;
} finally {
printWin.close();
}
}
// electron/services/htmlImport.ts
import { dialog } from "electron";
import path from "path";
import fs from "fs";
import log from "electron-log";
var selectHtmlFile = async (win) => {
if (!win) return { success: false, error: "No window found" };
try {
const { filePaths, canceled } = await dialog.showOpenDialog(win, {
title: "\u9009\u62E9 HTML \u6587\u4EF6",
filters: [
{ name: "HTML Files", extensions: ["html", "htm"] }
],
properties: ["openFile"]
});
if (canceled || filePaths.length === 0) {
return { success: false, canceled: true };
}
const htmlPath = filePaths[0];
const htmlDir = path.dirname(htmlPath);
const htmlFileName = path.basename(htmlPath, path.extname(htmlPath));
const assetsDirName = `${htmlFileName}_files`;
const assetsDirPath = path.join(htmlDir, assetsDirName);
const assetsFiles = [];
if (fs.existsSync(assetsDirPath)) {
const collectFiles = (dir, baseDir) => {
const entries = fs.readdirSync(dir, { withFileTypes: true });
for (const entry of entries) {
const fullPath = path.join(dir, entry.name);
if (entry.isDirectory()) {
collectFiles(fullPath, baseDir);
} else {
const relPath = path.relative(baseDir, fullPath);
assetsFiles.push(relPath);
}
}
};
collectFiles(assetsDirPath, assetsDirPath);
}
return {
success: true,
htmlPath,
htmlDir,
htmlFileName,
assetsDirName,
assetsFiles
};
} catch (error) {
log.error("Select HTML file failed:", error);
return { success: false, error: error.message };
}
};
// electron/state.ts
var ElectronState = class {
constructor() {
__publicField(this, "state", {
mainWindow: null,
serverPort: 3001,
isDev: false
});
}
getMainWindow() {
return this.state.mainWindow;
}
setMainWindow(window) {
this.state.mainWindow = window;
}
getServerPort() {
return this.state.serverPort;
}
setServerPort(port) {
this.state.serverPort = port;
}
isDevelopment() {
return this.state.isDev;
}
setDevelopment(isDev) {
this.state.isDev = isDev;
}
reset() {
this.state = {
mainWindow: null,
serverPort: 3001,
isDev: false
};
}
};
var electronState = new ElectronState();
// electron/main.ts
log2.initialize();
var __filename = fileURLToPath(import.meta.url);
var __dirname = path2.dirname(__filename);
process.env.NOTEBOOK_ROOT = path2.join(app.getPath("documents"), "XCDesktop");
if (!fs2.existsSync(process.env.NOTEBOOK_ROOT)) {
try {
fs2.mkdirSync(process.env.NOTEBOOK_ROOT, { recursive: true });
} catch (err) {
log2.error("Failed to create notebook directory:", err);
}
}
electronState.setDevelopment(!app.isPackaged);
var lastClipboardText = "";
function startClipboardWatcher() {
lastClipboardText = clipboard.readText();
setInterval(() => {
try {
const currentText = clipboard.readText();
if (currentText && currentText !== lastClipboardText) {
lastClipboardText = currentText;
log2.info("Clipboard changed, syncing to remote");
const win = electronState.getMainWindow();
if (win) {
win.webContents.send("remote-clipboard-auto-sync", currentText);
}
}
} catch (e) {
}
}, 1e3);
}
async function createWindow() {
const initialSymbolColor = nativeTheme.shouldUseDarkColors ? "#ffffff" : "#000000";
const mainWindow = new BrowserWindow3({
width: 1280,
height: 800,
minWidth: 1600,
minHeight: 900,
autoHideMenuBar: true,
titleBarStyle: "hidden",
titleBarOverlay: {
color: "#00000000",
symbolColor: initialSymbolColor,
height: 32
},
webPreferences: {
nodeIntegration: false,
contextIsolation: true,
sandbox: false,
webviewTag: true,
preload: path2.join(__dirname, "preload.cjs")
}
});
electronState.setMainWindow(mainWindow);
mainWindow.setMenu(null);
mainWindow.webContents.setWindowOpenHandler(({ url }) => {
if (url.startsWith("http:") || url.startsWith("https:")) {
shell.openExternal(url);
return { action: "deny" };
}
return { action: "allow" };
});
if (electronState.isDevelopment()) {
log2.info("Loading development URL...");
try {
await mainWindow.loadURL("http://localhost:5173");
} catch (e) {
log2.error("Failed to load dev URL. Make sure npm run electron:dev is used.", e);
}
mainWindow.webContents.openDevTools();
} else {
log2.info(`Loading production URL with port ${electronState.getServerPort()}...`);
await mainWindow.loadURL(`http://localhost:${electronState.getServerPort()}`);
}
}
ipcMain.handle("export-pdf", async (event, title, htmlContent) => {
const win = BrowserWindow3.fromWebContents(event.sender);
if (!win) return { success: false, error: "No window found" };
try {
const { filePath } = await dialog2.showSaveDialog(win, {
title: "\u5BFC\u51FA PDF",
defaultPath: `${title}.pdf`,
filters: [{ name: "PDF Files", extensions: ["pdf"] }]
});
if (!filePath) return { success: false, canceled: true };
if (!htmlContent) {
throw new Error("No HTML content provided for PDF export");
}
const pdfData = await generatePdf(htmlContent);
fs2.writeFileSync(filePath, pdfData);
return { success: true, filePath };
} catch (error) {
log2.error("Export PDF failed:", error);
return { success: false, error: error.message };
}
});
ipcMain.handle("select-html-file", async (event) => {
const win = BrowserWindow3.fromWebContents(event.sender);
return selectHtmlFile(win);
});
ipcMain.handle("update-titlebar-buttons", async (event, symbolColor) => {
const win = BrowserWindow3.fromWebContents(event.sender);
if (win) {
win.setTitleBarOverlay({ symbolColor });
return { success: true };
}
return { success: false };
});
ipcMain.handle("clipboard-read-text", async () => {
try {
const text = clipboard.readText();
return { success: true, text };
} catch (error) {
log2.error("Clipboard read failed:", error);
return { success: false, error: error.message };
}
});
ipcMain.handle("clipboard-write-text", async (event, text) => {
try {
clipboard.writeText(text);
return { success: true };
} catch (error) {
log2.error("Clipboard write failed:", error);
return { success: false, error: error.message };
}
});
ipcMain.handle("remote-fetch-drives", async (_event, serverHost, port, password) => {
try {
let url = `http://${serverHost}:${port}/api/files/drives`;
if (password) {
url += `?password=${encodeURIComponent(password)}`;
}
const response = await fetch(url);
if (!response.ok) {
throw new Error(`Failed to fetch drives: ${response.statusText}`);
}
const data = await response.json();
const items = data.items || [];
return {
success: true,
data: items.map((item) => ({
name: item.name,
path: item.name,
type: item.isDirectory ? "dir" : "file",
size: item.size,
modified: ""
}))
};
} catch (error) {
log2.error("Remote fetch drives failed:", error);
return { success: false, error: error.message };
}
});
ipcMain.handle("remote-fetch-files", async (_event, serverHost, port, filePath, password) => {
try {
let url = `http://${serverHost}:${port}/api/files/browse?path=${encodeURIComponent(filePath)}&allowSystem=true`;
if (password) {
url += `&password=${encodeURIComponent(password)}`;
}
const response = await fetch(url);
if (!response.ok) {
throw new Error(`Failed to fetch files: ${response.statusText}`);
}
const data = await response.json();
const items = data.items || [];
return {
success: true,
data: items.map((item) => ({
name: item.name,
path: data.currentPath ? `${data.currentPath}/${item.name}` : item.name,
type: item.isDirectory ? "dir" : "file",
size: item.size,
modified: item.modified?.toString()
}))
};
} catch (error) {
log2.error("Remote fetch files failed:", error);
return { success: false, error: error.message };
}
});
ipcMain.handle("remote-upload-file", async (_event, id, serverHost, port, filePath, remotePath, password) => {
try {
const win = electronState.getMainWindow();
if (!win) {
throw new Error("No window found");
}
const fullPath = path2.resolve(filePath);
if (!fs2.existsSync(fullPath)) {
throw new Error("File not found");
}
const stats = fs2.statSync(fullPath);
const fileSize = stats.size;
const fileName = path2.basename(fullPath);
let url = `http://${serverHost}:${port}/api/files/upload/start`;
if (password) {
url += `?password=${encodeURIComponent(password)}`;
}
const startResponse = await fetch(url, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ filename: fileName, fileSize })
});
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) {
log2.error("Remote upload failed:", error);
return { success: false, error: error.message };
}
});
ipcMain.handle("remote-download-file", async (_event, id, serverHost, port, fileName, remotePath, localPath, password) => {
try {
log2.info("Remote download params:", { id, serverHost, port, fileName, remotePath, localPath, password });
const win = electronState.getMainWindow();
if (!win) {
throw new Error("No window found");
}
const fullRemotePath = remotePath ? `${remotePath}\\${fileName}` : fileName;
let url = `http://${serverHost}:${port}/api/files/${encodeURIComponent(fullRemotePath)}`;
if (password) {
url += `?password=${encodeURIComponent(password)}`;
}
const response = await fetch(url);
if (!response.ok) {
throw new Error(`Download failed: ${response.statusText}`);
}
const contentLength = response.headers.get("Content-Length");
if (!contentLength) {
throw new Error("Server did not return Content-Length");
}
const totalSize = parseInt(contentLength, 10);
const targetDir = localPath || "C:\\";
const targetPath = path2.join(targetDir, fileName);
if (!fs2.existsSync(targetDir)) {
fs2.mkdirSync(targetDir, { recursive: true });
}
const fileStream = fs2.createWriteStream(targetPath);
const reader = response.body?.getReader();
if (!reader) {
throw new Error("Failed to get response body reader");
}
let downloadedSize = 0;
const CHUNK_SIZE = 64 * 1024;
while (true) {
const { done, value } = await reader.read();
if (done) {
break;
}
if (value) {
downloadedSize += value.length;
fileStream.write(value);
const progress = Math.round(downloadedSize / totalSize * 100);
win.webContents.send("download-progress", { id, progress });
}
}
fileStream.end();
return { success: true, filePath: targetPath };
} catch (error) {
log2.error("Remote download failed:", error);
return { success: false, error: error.message };
}
});
async function startServer() {
if (electronState.isDevelopment()) {
log2.info("In dev mode, assuming external servers are running.");
return;
}
const serverPath = path2.join(__dirname, "../dist-api/server.js");
const serverUrl = pathToFileURL(serverPath).href;
log2.info(`Starting internal server from: ${serverPath}`);
try {
const serverModule = await import(serverUrl);
if (serverModule.startServer) {
const port = await serverModule.startServer();
electronState.setServerPort(port);
log2.info(`Internal server started successfully on port ${port}`);
} else {
log2.warn("startServer function not found in server module, using default port 3001");
}
} catch (e) {
log2.error("Failed to start internal server:", e);
}
}
app.whenReady().then(async () => {
await startServer();
await createWindow();
startClipboardWatcher();
globalShortcut.register("CommandOrControl+Shift+C", () => {
log2.info("Global shortcut: sync clipboard to remote");
const win = electronState.getMainWindow();
if (win) {
win.webContents.send("remote-clipboard-sync-to-remote");
}
});
globalShortcut.register("CommandOrControl+Shift+V", () => {
log2.info("Global shortcut: sync clipboard from remote");
const win = electronState.getMainWindow();
if (win) {
win.webContents.send("remote-clipboard-sync-from-remote");
}
});
app.on("activate", () => {
if (BrowserWindow3.getAllWindows().length === 0) {
createWindow();
}
});
});
app.on("window-all-closed", () => {
globalShortcut.unregisterAll();
if (process.platform !== "darwin") {
app.quit();
}
});
//# sourceMappingURL=main.js.map