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