os.js 8.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228
  1. import { getMainWindow } from "ee-core/electron";
  2. import { app, dialog, shell } from "electron";
  3. import { logger } from "ee-core/log";
  4. import path from "path";
  5. import fs from "fs";
  6. import http from "http";
  7. import https from "https";
  8. import FormData from "form-data";
  9. class Os {
  10. async minimizeWindow() {
  11. getMainWindow().minimize();
  12. }
  13. async maximizeWindow() {
  14. getMainWindow().maximize();
  15. }
  16. async unmaximizeWindow() {
  17. getMainWindow().unmaximize();
  18. }
  19. async closeWindow() {
  20. getMainWindow().close();
  21. }
  22. async isMaximized() {
  23. return getMainWindow().isMaximized();
  24. }
  25. async toggleDevTools() {
  26. getMainWindow().webContents.toggleDevTools();
  27. }
  28. async openUserDataFolder() {
  29. const userDataPath = app.getPath('userData');
  30. await shell.openPath(userDataPath);
  31. }
  32. async openInstallFolder() {
  33. const exePath = app.getPath('exe');
  34. const installDir = path.dirname(exePath);
  35. await shell.openPath(installDir);
  36. }
  37. async selectDirectory() {
  38. const win = getMainWindow();
  39. const result = await dialog.showOpenDialog(win, {
  40. title: '选择目录',
  41. properties: ['openDirectory', 'createDirectory'],
  42. });
  43. if (result.canceled || !result.filePaths || result.filePaths.length === 0) {
  44. return '';
  45. }
  46. return result.filePaths[0];
  47. }
  48. /**
  49. * 按偏移分块读本机文件,返回 base64(离线地图瓦片这类大文件只按需取块)。
  50. *
  51. * <p>渲染进程侧的实现见 core/utils/map/offlineMap.ts 的 ElectronPmtilesSource:
  52. * PMTiles 只要求 getBytes(offset, length),所以这里只做「绝对路径 + 真实文件 + 边界收敛」。
  53. *
  54. * @param params { filePath: string, offset?: number, length: number }
  55. * @returns { data: base64, size: 实际读到的字节数, fileSize: 文件总大小 }
  56. */
  57. async readBinaryFileRange(params = {}) {
  58. const filePath = typeof params?.filePath === 'string' ? params.filePath.trim() : '';
  59. if (!filePath) throw new Error('文件路径不能为空');
  60. if (!path.isAbsolute(filePath)) throw new Error('只支持绝对路径');
  61. if (!fs.existsSync(filePath)) throw new Error(`文件不存在:${filePath}`);
  62. if (!fs.statSync(filePath).isFile()) throw new Error('目标是目录,不是文件');
  63. const fileSize = fs.statSync(filePath).size;
  64. const offset = Math.max(0, Math.floor(Number(params?.offset) || 0));
  65. const requested = Math.floor(Number(params?.length) || 0);
  66. if (!requested || requested < 0) throw new Error('读取长度不合法');
  67. if (offset >= fileSize) return { data: '', size: 0, fileSize };
  68. const length = Math.min(requested, fileSize - offset);
  69. const fd = fs.openSync(filePath, 'r');
  70. try {
  71. const buffer = Buffer.alloc(length);
  72. const read = fs.readSync(fd, buffer, 0, length, offset);
  73. return { data: buffer.subarray(0, read).toString('base64'), size: read, fileSize };
  74. } finally {
  75. fs.closeSync(fd);
  76. }
  77. }
  78. /**
  79. * 读本机文本文件(离线地图样式 JSON 等)。
  80. * @param params { filePath: string, encoding?: string }
  81. */
  82. async readTextFile(params = {}) {
  83. const filePath = typeof params?.filePath === 'string' ? params.filePath.trim() : '';
  84. if (!filePath) throw new Error('文件路径不能为空');
  85. if (!path.isAbsolute(filePath)) throw new Error('只支持绝对路径');
  86. if (!fs.existsSync(filePath)) throw new Error(`文件不存在:${filePath}`);
  87. if (!fs.statSync(filePath).isFile()) throw new Error('目标是目录,不是文件');
  88. const encoding = typeof params?.encoding === 'string' && params.encoding ? params.encoding : 'utf8';
  89. return fs.readFileSync(filePath, encoding);
  90. }
  91. async getAppInfo() {
  92. return {
  93. version: app.getVersion(),
  94. name: app.getName(),
  95. electronVersion: process.versions.electron,
  96. chromeVersion: process.versions.chrome,
  97. nodeVersion: process.versions.node,
  98. platform: process.platform,
  99. arch: process.arch,
  100. userDataPath: app.getPath('userData'),
  101. exePath: app.getPath('exe'),
  102. };
  103. }
  104. async openExternal(url) {
  105. if (!url || typeof url !== 'string') return false;
  106. try {
  107. await shell.openExternal(url);
  108. return true;
  109. } catch (e) {
  110. logger.error('[os] openExternal failed:', e);
  111. return false;
  112. }
  113. }
  114. /**
  115. * 通过 Node.js 主进程流式上传清鉴数据源文件
  116. * - 校验选中目录最后一级必须为 QingJian
  117. * - 在 QingJian 目录下拼接 workspace 作为扫描根
  118. * - 扫描 workspace 下一层文件夹,再读取每个文件夹下的文件(不递归更深层)
  119. * - 用 fs.createReadStream 流式读取,避免大文件占内存
  120. * - 用 form-data 构建 multipart,http/https 模块发送
  121. * @param params.dirPath 目录完整磁盘路径(最后一级必须为 QingJian)
  122. * @param params.uploadUrl 后端接口完整 URL
  123. * @param params.token 鉴权 token(可选)
  124. */
  125. async uploadQingJianFiles(params) {
  126. logger.info('[os] uploadQingJianFiles params:', params);
  127. const { dirPath, uploadUrl, token } = params || {};
  128. if (!dirPath || !uploadUrl) {
  129. logger.error('[os] uploadQingJianFiles params missing:', params);
  130. throw new Error('请选择 QingJian 目录');
  131. }
  132. // 1. 校验选中目录最后一级必须为 QingJian
  133. const dirName = path.basename(dirPath);
  134. if (dirName !== 'QingJian') {
  135. throw new Error('请选择 QingJian 目录');
  136. }
  137. // 2. 在选中目录下拼接 workspace,作为扫描根目录
  138. const workspacePath = path.join(dirPath, 'workspace');
  139. const platPath = path.join(dirPath, 'db',"plat");
  140. // 3. 扫描 workspace 下一层文件夹,再读取每个文件夹下的文件(不递归更深层)
  141. // id = 文件直接所在的父级目录名(即子文件夹名)
  142. const subEntries = await fs.promises.readdir(workspacePath, { withFileTypes: true });
  143. const subDirs = subEntries.filter(e => e.isDirectory());
  144. if (subDirs.length === 0) {
  145. throw new Error('workspace 目录下没有子文件夹');
  146. }
  147. const files = [];
  148. for (const sub of subDirs) {
  149. const subDirPath = path.join(workspacePath, sub.name);
  150. const subFiles = await fs.promises.readdir(subDirPath, { withFileTypes: true });
  151. for (const f of subFiles) {
  152. if (f.isFile()) {
  153. files.push({ id: sub.name, name: f.name, filePath: path.join(subDirPath, f.name) });
  154. }
  155. }
  156. }
  157. files.push({ id: 'plat', name: 'plat', filePath: platPath });
  158. if (files.length === 0) {
  159. throw new Error('workspace 的子文件夹下没有可上传的文件');
  160. }
  161. // 4. 构建 multipart FormData
  162. const form = new FormData();
  163. form.append('path', dirPath);
  164. for (let i = 0; i < files.length; i++) {
  165. const { id, name: fileName, filePath } = files[i];
  166. const stream = fs.createReadStream(filePath);
  167. form.append(`items[${i}].file`, stream, { filename: fileName });
  168. form.append(`items[${i}].name`, fileName);
  169. form.append(`items[${i}].id`, id);
  170. }
  171. // 3. 发送 HTTP 请求(流式 pipe,不占内存)
  172. return await new Promise((resolve, reject) => {
  173. const urlObj = new URL(uploadUrl);
  174. const lib = urlObj.protocol === 'https:' ? https : http;
  175. const headers = form.getHeaders();
  176. if (token) {
  177. headers['Authorization'] = token;
  178. }
  179. const req = lib.request(uploadUrl, {
  180. method: 'POST',
  181. headers,
  182. }, (res) => {
  183. let data = '';
  184. res.setEncoding('utf8');
  185. res.on('data', (chunk) => { data += chunk; });
  186. res.on('end', () => {
  187. const status = res.statusCode || 0;
  188. if (status >= 200 && status < 300) {
  189. try {
  190. resolve(JSON.parse(data));
  191. } catch {
  192. resolve(data);
  193. }
  194. } else {
  195. reject(new Error(`HTTP ${status}: ${data}`));
  196. }
  197. });
  198. });
  199. req.on('error', (e) => {
  200. logger.error('[os] uploadQingJianFiles request error:', e);
  201. reject(e.message);
  202. });
  203. form.pipe(req);
  204. });
  205. }
  206. }
  207. Os.toString = () => 'Os';
  208. export const os = new Os();