Home Reference Source

cables_dev/cables_electron/src/utils/projects_util.js

  1. import { SharedProjectsUtil, utilProvider } from "cables-shared-api";
  2. import path from "path";
  3. import sanitizeFileName from "sanitize-filename";
  4. import { app } from "electron";
  5. import pako from "pako";
  6. import crypto from "crypto";
  7. import jsonfile from "jsonfile";
  8. import fs from "fs";
  9. import settings from "../electron/electron_settings.js";
  10. import helper from "./helper_util.js";
  11. import cables from "../cables.js";
  12. import filesUtil from "./files_util.js";
  13. import opsUtil from "./ops_util.js";
  14.  
  15. class ProjectsUtil extends SharedProjectsUtil
  16. {
  17. constructor(provider)
  18. {
  19. super(provider);
  20. this.CABLES_PROJECT_FILE_EXTENSION = "cables";
  21.  
  22. this._dirInfos = null;
  23. this._projectOpDocs = null;
  24. }
  25.  
  26. getAssetPath(projectId)
  27. {
  28. return cables.getAssetPath();
  29. }
  30.  
  31. getAssetPathUrl(projectId)
  32. {
  33. return "./assets/";
  34. }
  35.  
  36. getScreenShotPath(pId)
  37. {
  38. return path.join(app.getPath("userData"), "screenshots/");
  39. }
  40.  
  41. getScreenShotFileName(proj, ext)
  42. {
  43. const screenShotPath = this.getScreenShotPath(proj.id);
  44. return path.join(screenShotPath, "/", filesUtil.realSanitizeFilename(proj.name) + "." + ext);
  45. }
  46.  
  47. generateNewProject(owner)
  48. {
  49. if (!owner) owner = settings.getCurrentUser();
  50. const now = Date.now();
  51. const projectId = helper.generateRandomId();
  52. const shortId = helper.generateShortId(projectId, now);
  53. const randomize = settings.getUserSetting("randomizePatchName", false);
  54. const newProjectName = this.getNewProjectName(randomize);
  55.  
  56. return {
  57. "_id": projectId,
  58. "shortId": shortId,
  59. "name": newProjectName,
  60. "description": "",
  61. "userId": owner._id,
  62. "cachedUsername": owner.username,
  63. "created": now,
  64. "updated": now,
  65. "visibility": "private",
  66. "ops": [],
  67. "settings": {
  68. "licence": "none"
  69. },
  70. "userList": [owner],
  71. "teams": [],
  72. "log": []
  73. };
  74. }
  75.  
  76. getNewProjectName(randomize = false)
  77. {
  78. return "untitled";
  79. }
  80.  
  81. getProjectOpDirs(project, includeOsDir = true, reverse = false, addLocalCoreIfPackaged = true)
  82. {
  83. let opsDirs = [];
  84.  
  85. const projectDir = settings.getCurrentProjectDir();
  86. if (projectDir)
  87. {
  88. const currentDir = path.join(projectDir, "ops/");
  89. opsDirs.push(currentDir);
  90. }
  91.  
  92. if (project && project.dirs && project.dirs.ops)
  93. {
  94. project.dirs.ops.forEach((dir) =>
  95. {
  96. if (projectDir && !path.isAbsolute(dir)) dir = path.join(projectDir, dir);
  97. opsDirs.push(dir);
  98. });
  99. }
  100. if (includeOsDir)
  101. {
  102. const osOpsDir = cables.getOsOpsDir();
  103. if (osOpsDir) opsDirs.push(osOpsDir);
  104. }
  105. if (addLocalCoreIfPackaged && !cables.isPackaged())
  106. {
  107. opsDirs.push(cables.getExtensionOpsPath());
  108. opsDirs.push(cables.getCoreOpsPath());
  109. }
  110. opsDirs = helper.uniqueArray(opsDirs);
  111. if (reverse) return opsDirs.reverse();
  112. return opsDirs;
  113. }
  114.  
  115. isFixedPositionOpDir(dir)
  116. {
  117. const projectDir = settings.getCurrentProjectDir();
  118. if (projectDir) if (dir === path.join(projectDir, "ops/")) return true;
  119. if (dir === "./ops") return true;
  120. if (dir === cables.getOsOpsDir()) return true;
  121. if (cables.isPackaged()) return false;
  122. if (dir === cables.getExtensionOpsPath()) return true;
  123. return dir === cables.getCoreOpsPath();
  124. }
  125.  
  126. getProjectFileName(project)
  127. {
  128. return sanitizeFileName(project.name).replace(/ /g, "_") + "." + this.CABLES_PROJECT_FILE_EXTENSION;
  129. }
  130.  
  131. writeProjectToFile(projectFile, project = null, patch = null)
  132. {
  133. if (!project) project = this.generateNewProject();
  134. if (!project.ops) project.ops = [];
  135. if (patch && (patch.data || patch.dataB64))
  136. {
  137. try
  138. {
  139. let buf = patch.data;
  140. if (patch.dataB64) buf = Buffer.from(patch.dataB64, "base64");
  141.  
  142. const qData = JSON.parse(pako.inflate(buf, { "to": "string" }));
  143. if (qData.ops) project.ops = qData.ops;
  144. if (qData.ui) project.ui = qData.ui;
  145. }
  146. catch (e)
  147. {
  148. this._log.error("patch save error/invalid data", e);
  149. return;
  150. }
  151. }
  152.  
  153. // filter imported ops, so we do not save these to the database
  154. project.ops = project.ops.filter((op) =>
  155. {
  156. return !(op.storage && op.storage.blueprint);
  157. });
  158.  
  159. project.name = path.basename(projectFile, "." + this.CABLES_PROJECT_FILE_EXTENSION);
  160. project.summary = project.summary || {};
  161. project.summary.title = project.name;
  162.  
  163. project.opsHash = crypto
  164. .createHash("sha1")
  165. .update(JSON.stringify(project.ops))
  166. .digest("hex");
  167. project.buildInfo = settings.getBuildInfo();
  168. jsonfile.writeFileSync(projectFile, project, { "encoding": "utf-8", "spaces": 4 });
  169. settings.addToRecentProjects(projectFile, project);
  170. }
  171.  
  172. getUsedAssetFilenames(project, includeLibraryAssets = false)
  173. {
  174. const fileNames = [];
  175. if (!project || !project.ops) return [];
  176. const assetPorts = this.getProjectAssetPorts(project, includeLibraryAssets);
  177. let urls = assetPorts.map((assetPort) => { return helper.pathToFileURL(assetPort.value, true); });
  178. urls.forEach((url) =>
  179. {
  180. let fullPath = helper.fileURLToPath(url, true);
  181. if (fullPath && fs.existsSync(fullPath))
  182. {
  183. fileNames.push(fullPath);
  184. }
  185. });
  186. return helper.uniqueArray(fileNames);
  187. }
  188.  
  189. addOpDir(project, opDir, atTop = false)
  190. {
  191. if (!project.dirs) project.dirs = {};
  192. if (!project.dirs.ops) project.dirs.ops = [];
  193. if (atTop)
  194. {
  195. project.dirs.ops.unshift(opDir);
  196. }
  197. else
  198. {
  199. project.dirs.ops.push(opDir);
  200. }
  201. project.dirs.ops = helper.uniqueArray(project.dirs.ops);
  202. this.invalidateProjectCaches(opDir, atTop);
  203. return project;
  204. }
  205.  
  206. removeOpDir(project, opDir)
  207. {
  208. if (!project.dirs) project.dirs = {};
  209. if (!project.dirs.ops) project.dirs.ops = [];
  210. project.dirs.ops = project.dirs.ops.filter((dirName) =>
  211. {
  212. return dirName !== opDir;
  213. });
  214. project.dirs.ops = helper.uniqueArray(project.dirs.ops);
  215. this.invalidateProjectCaches(opDir);
  216. return project;
  217. }
  218.  
  219. getSummary(project)
  220. {
  221. if (!project) return {};
  222. return {
  223. "allowEdit": true,
  224. "title": project.name,
  225. "owner": settings.getCurrentUser(),
  226. "description": project.description,
  227. "licence": {
  228. "name": "No licence chosen"
  229. }
  230. };
  231. }
  232.  
  233. getOpDirs(currentProject)
  234. {
  235. const dirs = this.getProjectOpDirs(currentProject, true);
  236. const dirInfos = [];
  237.  
  238. dirs.forEach((dir) =>
  239. {
  240. const opJsons = helper.getFileNamesRecursive(dir, ".json");
  241. const opLocations = {};
  242. opJsons.forEach((jsonLocation) =>
  243. {
  244. const jsonName = path.basename(jsonLocation, ".json");
  245. if (opsUtil.isOpNameValid(jsonName))
  246. {
  247. opLocations[jsonName] = path.dirname(path.join(dir, jsonLocation));
  248. }
  249. });
  250. const opNames = Object.keys(opLocations);
  251.  
  252. dirInfos.push({
  253. "dir": dir,
  254. "opLocations": opLocations,
  255. "numOps": opNames.length,
  256. "fixedPlace": this.isFixedPositionOpDir(dir)
  257. });
  258. });
  259. return dirInfos;
  260. }
  261.  
  262. reorderOpDirs(currentProject, order)
  263. {
  264. const currentProjectFile = settings.getCurrentProjectFile();
  265. const newOrder = [];
  266. order.forEach((opDir) =>
  267. {
  268. if (fs.existsSync(opDir)) newOrder.push(opDir);
  269. });
  270. if (!currentProject.dirs) currentProject.dirs = {};
  271. if (!currentProject.dirs.ops) currentProject.dirs.ops = [];
  272. currentProject.dirs.ops = newOrder.filter((dir) => { return !this.isFixedPositionOpDir(dir); });
  273. currentProject.dirs.ops = helper.uniqueArray(currentProject.dirs.ops);
  274. this.writeProjectToFile(currentProjectFile, currentProject);
  275. this.invalidateProjectCaches();
  276. return currentProject;
  277. }
  278.  
  279. getAbsoluteOpDirFromHierarchy(opName)
  280. {
  281. const currentProject = settings.getCurrentProject();
  282. if (!this._dirInfos)
  283. {
  284. this._log.debug("rebuilding opdir-cache, changed by:", opName);
  285. this._dirInfos = this.getOpDirs(currentProject);
  286. }
  287. if (!this._dirInfos) return this._opsUtil.getOpSourceNoHierarchy(opName);
  288.  
  289. for (let i = 0; i < this._dirInfos.length; i++)
  290. {
  291. const dirInfo = this._dirInfos[i];
  292. const opNames = dirInfo.opLocations ? Object.keys(dirInfo.opLocations) : [];
  293. if (opNames.includes(opName))
  294. {
  295. return dirInfo.opLocations[opName];
  296. }
  297. }
  298. return this._opsUtil.getOpSourceNoHierarchy(opName);
  299. }
  300.  
  301. invalidateProjectCaches()
  302. {
  303. this._dirInfos = null;
  304. this._projectOpDocs = null;
  305. }
  306.  
  307. getOpDocsInProjectDirs(project, rebuildCache = false)
  308. {
  309. if (this._projectOpDocs && !rebuildCache) return this._projectOpDocs;
  310.  
  311. const opDocs = {};
  312. const opDirs = this.getProjectOpDirs(project, true, false, false);
  313.  
  314. opDirs.forEach((opDir) =>
  315. {
  316. if (fs.existsSync(opDir))
  317. {
  318. const opJsons = helper.getFilesRecursive(opDir, ".json");
  319. for (let jsonPath in opJsons)
  320. {
  321. const opName = path.basename(jsonPath, ".json");
  322. if (opsUtil.isOpNameValid(opName))
  323. {
  324. if (opDocs.hasOwnProperty(opName))
  325. {
  326. if (!opDocs[opName].hasOwnProperty("overrides")) opDocs[opName].overrides = [];
  327. opDocs[opName].overrides.push(path.join(opDir, path.dirname(jsonPath)));
  328. }
  329. else
  330. {
  331. try
  332. {
  333. const opDoc = jsonfile.readFileSync(path.join(opDir, jsonPath));
  334. opDoc.name = opName;
  335. opDocs[opName] = opDoc;
  336. }
  337. catch (e)
  338. {
  339. this._log.warn("failed to parse opDoc for", opName, "from", jsonPath);
  340. }
  341. }
  342. }
  343. }
  344. }
  345. });
  346. this._projectOpDocs = Object.values(opDocs);
  347. this._docsUtil.addOpsToLookup(this._projectOpDocs);
  348. return this._projectOpDocs;
  349. }
  350. }
  351. export default new ProjectsUtil(utilProvider);