cyberia 3.0.3 → 3.2.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (296) hide show
  1. package/{.env.production → .env.example} +20 -4
  2. package/.github/workflows/engine-cyberia.cd.yml +43 -10
  3. package/.github/workflows/engine-cyberia.ci.yml +48 -26
  4. package/.github/workflows/ghpkg.ci.yml +5 -5
  5. package/.github/workflows/gitlab.ci.yml +1 -1
  6. package/.github/workflows/hardhat.ci.yml +82 -0
  7. package/.github/workflows/npmpkg.ci.yml +60 -14
  8. package/.github/workflows/publish.ci.yml +26 -7
  9. package/.github/workflows/publish.cyberia.ci.yml +5 -5
  10. package/.github/workflows/pwa-microservices-template-page.cd.yml +6 -7
  11. package/.github/workflows/pwa-microservices-template-test.ci.yml +4 -4
  12. package/.github/workflows/release.cd.yml +14 -8
  13. package/.vscode/extensions.json +9 -8
  14. package/.vscode/settings.json +3 -2
  15. package/CHANGELOG.md +643 -1
  16. package/CLI-HELP.md +132 -57
  17. package/Dockerfile +4 -2
  18. package/README.md +347 -22
  19. package/WHITE-PAPER.md +1540 -0
  20. package/bin/build.js +21 -12
  21. package/bin/cyberia.js +2640 -106
  22. package/bin/deploy.js +258 -372
  23. package/bin/file.js +5 -1
  24. package/bin/index.js +2640 -106
  25. package/bin/vs.js +3 -3
  26. package/conf.js +169 -105
  27. package/deployment.yaml +236 -20
  28. package/hardhat/.env.example +31 -0
  29. package/hardhat/README.md +531 -0
  30. package/hardhat/WHITE-PAPER.md +1540 -0
  31. package/hardhat/contracts/ObjectLayerToken.sol +391 -0
  32. package/hardhat/deployments/.gitkeep +0 -0
  33. package/hardhat/deployments/hardhat-ObjectLayerToken.json +11 -0
  34. package/hardhat/hardhat.config.js +136 -0
  35. package/hardhat/ignition/modules/ObjectLayerToken.js +21 -0
  36. package/hardhat/networks/besu-object-layer.network.json +138 -0
  37. package/hardhat/package-lock.json +4323 -0
  38. package/hardhat/package.json +36 -0
  39. package/hardhat/scripts/deployObjectLayerToken.js +98 -0
  40. package/hardhat/test/ObjectLayerToken.js +592 -0
  41. package/hardhat/types/ethers-contracts/ObjectLayerToken.ts +690 -0
  42. package/hardhat/types/ethers-contracts/common.ts +92 -0
  43. package/hardhat/types/ethers-contracts/factories/ObjectLayerToken__factory.ts +1055 -0
  44. package/hardhat/types/ethers-contracts/factories/index.ts +4 -0
  45. package/hardhat/types/ethers-contracts/hardhat.d.ts +47 -0
  46. package/hardhat/types/ethers-contracts/index.ts +6 -0
  47. package/jsdoc.dd-cyberia.json +68 -0
  48. package/jsdoc.json +65 -49
  49. package/manifests/cronjobs/dd-cron/dd-cron-backup.yaml +5 -4
  50. package/manifests/cronjobs/dd-cron/dd-cron-dns.yaml +5 -4
  51. package/manifests/deployment/dd-cyberia-development/deployment.yaml +562 -0
  52. package/manifests/deployment/dd-cyberia-development/proxy.yaml +297 -0
  53. package/manifests/deployment/dd-cyberia-development/pv-pvc.yaml +132 -0
  54. package/manifests/deployment/dd-default-development/deployment.yaml +2 -2
  55. package/manifests/deployment/dd-test-development/deployment.yaml +88 -74
  56. package/manifests/deployment/dd-test-development/proxy.yaml +13 -4
  57. package/manifests/deployment/playwright/deployment.yaml +1 -1
  58. package/manifests/pv-pvc-dd.yaml +1 -1
  59. package/nodemon.json +1 -1
  60. package/package.json +60 -48
  61. package/proxy.yaml +118 -10
  62. package/pv-pvc.yaml +132 -0
  63. package/scripts/k3s-node-setup.sh +1 -1
  64. package/scripts/ports-ls.sh +2 -0
  65. package/scripts/rhel-grpc-setup.sh +56 -0
  66. package/src/api/atlas-sprite-sheet/atlas-sprite-sheet.controller.js +47 -1
  67. package/src/api/atlas-sprite-sheet/atlas-sprite-sheet.model.js +17 -2
  68. package/src/api/atlas-sprite-sheet/atlas-sprite-sheet.router.js +5 -0
  69. package/src/api/atlas-sprite-sheet/atlas-sprite-sheet.service.js +80 -7
  70. package/src/api/cyberia-dialogue/cyberia-dialogue.controller.js +93 -0
  71. package/src/api/cyberia-dialogue/cyberia-dialogue.model.js +36 -0
  72. package/src/api/cyberia-dialogue/cyberia-dialogue.router.js +29 -0
  73. package/src/api/cyberia-dialogue/cyberia-dialogue.service.js +51 -0
  74. package/src/api/cyberia-entity/cyberia-entity.controller.js +74 -0
  75. package/src/api/cyberia-entity/cyberia-entity.model.js +24 -0
  76. package/src/api/cyberia-entity/cyberia-entity.router.js +27 -0
  77. package/src/api/cyberia-entity/cyberia-entity.service.js +42 -0
  78. package/src/api/cyberia-instance/cyberia-fallback-world.js +368 -0
  79. package/src/api/cyberia-instance/cyberia-instance.controller.js +92 -0
  80. package/src/api/cyberia-instance/cyberia-instance.model.js +84 -0
  81. package/src/api/cyberia-instance/cyberia-instance.router.js +63 -0
  82. package/src/api/cyberia-instance/cyberia-instance.service.js +191 -0
  83. package/src/api/cyberia-instance/cyberia-portal-connector.js +486 -0
  84. package/src/api/cyberia-instance-conf/cyberia-instance-conf.controller.js +74 -0
  85. package/src/api/cyberia-instance-conf/cyberia-instance-conf.defaults.js +413 -0
  86. package/src/api/cyberia-instance-conf/cyberia-instance-conf.model.js +228 -0
  87. package/src/api/cyberia-instance-conf/cyberia-instance-conf.router.js +27 -0
  88. package/src/api/cyberia-instance-conf/cyberia-instance-conf.service.js +42 -0
  89. package/src/api/cyberia-map/cyberia-map.controller.js +79 -0
  90. package/src/api/cyberia-map/cyberia-map.model.js +30 -0
  91. package/src/api/cyberia-map/cyberia-map.router.js +40 -0
  92. package/src/api/cyberia-map/cyberia-map.service.js +74 -0
  93. package/src/api/document/document.service.js +1 -1
  94. package/src/api/file/file.controller.js +3 -1
  95. package/src/api/file/file.ref.json +18 -0
  96. package/src/api/file/file.service.js +28 -5
  97. package/src/api/ipfs/ipfs.controller.js +4 -25
  98. package/src/api/ipfs/ipfs.model.js +43 -34
  99. package/src/api/ipfs/ipfs.router.js +8 -13
  100. package/src/api/ipfs/ipfs.service.js +56 -104
  101. package/src/api/object-layer/README.md +347 -22
  102. package/src/api/object-layer/object-layer.controller.js +6 -2
  103. package/src/api/object-layer/object-layer.model.js +12 -8
  104. package/src/api/object-layer/object-layer.router.js +698 -42
  105. package/src/api/object-layer/object-layer.service.js +119 -37
  106. package/src/api/object-layer-render-frames/object-layer-render-frames.model.js +1 -2
  107. package/src/api/user/user.router.js +10 -5
  108. package/src/api/user/user.service.js +15 -14
  109. package/src/cli/baremetal.js +6 -10
  110. package/src/cli/cloud-init.js +0 -3
  111. package/src/cli/cluster.js +7 -7
  112. package/src/cli/db.js +723 -857
  113. package/src/cli/deploy.js +215 -105
  114. package/src/cli/env.js +34 -5
  115. package/src/cli/fs.js +5 -4
  116. package/src/cli/image.js +0 -3
  117. package/src/cli/index.js +83 -15
  118. package/src/cli/kubectl.js +211 -0
  119. package/src/cli/monitor.js +5 -6
  120. package/src/cli/release.js +284 -0
  121. package/src/cli/repository.js +708 -62
  122. package/src/cli/run.js +371 -151
  123. package/src/cli/secrets.js +73 -2
  124. package/src/cli/ssh.js +1 -1
  125. package/src/cli/test.js +3 -3
  126. package/src/client/Cryptokoyn.index.js +3 -4
  127. package/src/client/CyberiaPortal.index.js +3 -4
  128. package/src/client/Default.index.js +3 -4
  129. package/src/client/Itemledger.index.js +4 -963
  130. package/src/client/Underpost.index.js +3 -4
  131. package/src/client/components/core/AgGrid.js +20 -5
  132. package/src/client/components/core/Alert.js +2 -2
  133. package/src/client/components/core/AppStore.js +69 -0
  134. package/src/client/components/core/CalendarCore.js +2 -2
  135. package/src/client/components/core/Content.js +22 -3
  136. package/src/client/components/core/Docs.js +30 -6
  137. package/src/client/components/core/DropDown.js +137 -17
  138. package/src/client/components/core/FileExplorer.js +71 -4
  139. package/src/client/components/core/Input.js +1 -1
  140. package/src/client/components/core/Keyboard.js +2 -2
  141. package/src/client/components/core/LogIn.js +2 -2
  142. package/src/client/components/core/LogOut.js +2 -2
  143. package/src/client/components/core/Modal.js +20 -7
  144. package/src/client/components/core/Panel.js +0 -1
  145. package/src/client/components/core/PanelForm.js +19 -19
  146. package/src/client/components/core/RichText.js +1 -2
  147. package/src/client/components/core/SocketIo.js +82 -29
  148. package/src/client/components/core/SocketIoHandler.js +75 -0
  149. package/src/client/components/core/Stream.js +143 -95
  150. package/src/client/components/core/Webhook.js +40 -7
  151. package/src/client/components/cryptokoyn/AppStoreCryptokoyn.js +5 -0
  152. package/src/client/components/cryptokoyn/LogInCryptokoyn.js +3 -3
  153. package/src/client/components/cryptokoyn/LogOutCryptokoyn.js +2 -2
  154. package/src/client/components/cryptokoyn/MenuCryptokoyn.js +3 -3
  155. package/src/client/components/cryptokoyn/SocketIoCryptokoyn.js +3 -51
  156. package/src/client/components/cyberia/InstanceEngineCyberia.js +700 -0
  157. package/src/client/components/cyberia/MapEngineCyberia.js +1359 -2
  158. package/src/client/components/cyberia/ObjectLayerEngineModal.js +17 -6
  159. package/src/client/components/cyberia/ObjectLayerEngineViewer.js +92 -54
  160. package/src/client/components/cyberia-portal/AppStoreCyberiaPortal.js +5 -0
  161. package/src/client/components/cyberia-portal/CommonCyberiaPortal.js +217 -30
  162. package/src/client/components/cyberia-portal/CssCyberiaPortal.js +44 -2
  163. package/src/client/components/cyberia-portal/LogInCyberiaPortal.js +3 -4
  164. package/src/client/components/cyberia-portal/LogOutCyberiaPortal.js +2 -2
  165. package/src/client/components/cyberia-portal/MenuCyberiaPortal.js +104 -9
  166. package/src/client/components/cyberia-portal/RoutesCyberiaPortal.js +5 -0
  167. package/src/client/components/cyberia-portal/SocketIoCyberiaPortal.js +3 -49
  168. package/src/client/components/cyberia-portal/TranslateCyberiaPortal.js +4 -0
  169. package/src/client/components/default/AppStoreDefault.js +5 -0
  170. package/src/client/components/default/LogInDefault.js +3 -3
  171. package/src/client/components/default/LogOutDefault.js +2 -2
  172. package/src/client/components/default/MenuDefault.js +5 -5
  173. package/src/client/components/default/SocketIoDefault.js +3 -51
  174. package/src/client/components/itemledger/AppStoreItemledger.js +5 -0
  175. package/src/client/components/itemledger/LogInItemledger.js +3 -3
  176. package/src/client/components/itemledger/LogOutItemledger.js +2 -2
  177. package/src/client/components/itemledger/MenuItemledger.js +3 -3
  178. package/src/client/components/itemledger/SocketIoItemledger.js +3 -51
  179. package/src/client/components/underpost/AppStoreUnderpost.js +5 -0
  180. package/src/client/components/underpost/CssUnderpost.js +59 -0
  181. package/src/client/components/underpost/LogInUnderpost.js +6 -3
  182. package/src/client/components/underpost/LogOutUnderpost.js +4 -2
  183. package/src/client/components/underpost/MenuUnderpost.js +104 -18
  184. package/src/client/components/underpost/RoutesUnderpost.js +2 -0
  185. package/src/client/components/underpost/SocketIoUnderpost.js +3 -51
  186. package/src/client/public/cryptokoyn/assets/logo/base-icon.png +0 -0
  187. package/src/client/public/cryptokoyn/browserconfig.xml +12 -0
  188. package/src/client/public/cryptokoyn/microdata.json +85 -0
  189. package/src/client/public/cryptokoyn/site.webmanifest +57 -0
  190. package/src/client/public/cryptokoyn/sitemap +3 -3
  191. package/src/client/public/default/sitemap +3 -3
  192. package/src/client/public/itemledger/browserconfig.xml +2 -2
  193. package/src/client/public/itemledger/manifest.webmanifest +4 -4
  194. package/src/client/public/itemledger/microdata.json +71 -0
  195. package/src/client/public/itemledger/sitemap +3 -3
  196. package/src/client/public/itemledger/yandex-browser-manifest.json +2 -2
  197. package/src/client/public/test/sitemap +3 -3
  198. package/src/client/services/core/core.service.js +20 -8
  199. package/src/client/services/cyberia-dialogue/cyberia-dialogue.service.js +105 -0
  200. package/src/client/services/cyberia-entity/cyberia-entity.management.js +57 -0
  201. package/src/client/services/cyberia-entity/cyberia-entity.service.js +105 -0
  202. package/src/client/services/cyberia-instance/cyberia-instance.management.js +194 -0
  203. package/src/client/services/cyberia-instance/cyberia-instance.service.js +122 -0
  204. package/src/client/services/cyberia-instance-conf/cyberia-instance-conf.service.js +105 -0
  205. package/src/client/services/cyberia-map/cyberia-map.management.js +193 -0
  206. package/src/client/services/cyberia-map/cyberia-map.service.js +126 -0
  207. package/src/client/services/instance/instance.management.js +2 -2
  208. package/src/client/services/ipfs/ipfs.service.js +3 -23
  209. package/src/client/services/object-layer/object-layer.management.js +3 -3
  210. package/src/client/services/object-layer/object-layer.service.js +21 -0
  211. package/src/client/services/user/user.management.js +2 -2
  212. package/src/client/ssr/body/404.js +15 -11
  213. package/src/client/ssr/body/500.js +15 -11
  214. package/src/client/ssr/body/SwaggerDarkMode.js +285 -0
  215. package/src/client/ssr/head/PwaItemledger.js +60 -0
  216. package/src/client/ssr/offline/NoNetworkConnection.js +11 -10
  217. package/src/client/ssr/pages/CyberiaServerMetrics.js +1 -1
  218. package/src/client/ssr/pages/Test.js +11 -10
  219. package/src/client.build.js +0 -3
  220. package/src/client.dev.js +0 -3
  221. package/src/db/DataBaseProvider.js +17 -2
  222. package/src/db/mariadb/MariaDB.js +14 -9
  223. package/src/db/mongo/MongooseDB.js +17 -1
  224. package/src/grpc/cyberia/OFF_CHAIN_ECONOMY.md +305 -0
  225. package/src/grpc/cyberia/README.md +326 -0
  226. package/src/grpc/cyberia/grpc-server.js +530 -0
  227. package/src/index.js +24 -1
  228. package/src/proxy.js +0 -3
  229. package/src/runtime/express/Dockerfile +4 -0
  230. package/src/runtime/express/Express.js +33 -10
  231. package/src/runtime/lampp/Dockerfile +13 -2
  232. package/src/runtime/lampp/Lampp.js +33 -17
  233. package/src/runtime/wp/Dockerfile +68 -0
  234. package/src/runtime/wp/Wp.js +639 -0
  235. package/src/server/auth.js +36 -15
  236. package/src/server/backup.js +39 -12
  237. package/src/server/besu-genesis-generator.js +1630 -0
  238. package/src/server/client-build-docs.js +133 -17
  239. package/src/server/client-build-live.js +9 -18
  240. package/src/server/client-build.js +229 -101
  241. package/src/server/client-dev-server.js +14 -13
  242. package/src/server/client-formatted.js +109 -57
  243. package/src/server/conf.js +391 -164
  244. package/src/server/cron.js +27 -24
  245. package/src/server/dns.js +29 -12
  246. package/src/server/downloader.js +0 -2
  247. package/src/server/ipfs-client.js +24 -1
  248. package/src/server/logger.js +27 -9
  249. package/src/server/object-layer.js +217 -103
  250. package/src/server/peer.js +8 -2
  251. package/src/server/process.js +1 -50
  252. package/src/server/proxy.js +4 -8
  253. package/src/server/runtime.js +30 -9
  254. package/src/server/semantic-layer-generator-floor.js +359 -0
  255. package/src/server/semantic-layer-generator-skin.js +1294 -0
  256. package/src/server/semantic-layer-generator.js +116 -555
  257. package/src/server/ssr.js +0 -3
  258. package/src/server/start.js +19 -12
  259. package/src/server/tls.js +0 -2
  260. package/src/server.js +0 -4
  261. package/src/ws/IoInterface.js +1 -10
  262. package/src/ws/IoServer.js +14 -33
  263. package/src/ws/core/channels/core.ws.chat.js +65 -20
  264. package/src/ws/core/channels/core.ws.mailer.js +113 -32
  265. package/src/ws/core/channels/core.ws.stream.js +90 -31
  266. package/src/ws/core/core.ws.connection.js +12 -33
  267. package/src/ws/core/core.ws.emit.js +10 -26
  268. package/src/ws/core/core.ws.server.js +25 -58
  269. package/src/ws/default/channels/default.ws.main.js +53 -12
  270. package/src/ws/default/default.ws.connection.js +26 -13
  271. package/src/ws/default/default.ws.server.js +30 -12
  272. package/.env.development +0 -43
  273. package/.env.test +0 -43
  274. package/hardhat/contracts/CryptoKoyn.sol +0 -59
  275. package/hardhat/contracts/ItemLedger.sol +0 -73
  276. package/hardhat/contracts/Lock.sol +0 -34
  277. package/hardhat/hardhat.config.cjs +0 -45
  278. package/hardhat/ignition/modules/Lock.js +0 -18
  279. package/hardhat/networks/cryptokoyn-itemledger.network.json +0 -29
  280. package/hardhat/scripts/deployCryptokoyn.cjs +0 -25
  281. package/hardhat/scripts/deployItemledger.cjs +0 -25
  282. package/hardhat/test/Lock.js +0 -126
  283. package/hardhat/white-paper.md +0 -581
  284. package/src/client/components/cryptokoyn/CommonCryptokoyn.js +0 -29
  285. package/src/client/components/cryptokoyn/ElementsCryptokoyn.js +0 -38
  286. package/src/client/components/cyberia-portal/ElementsCyberiaPortal.js +0 -38
  287. package/src/client/components/default/ElementsDefault.js +0 -38
  288. package/src/client/components/itemledger/CommonItemledger.js +0 -29
  289. package/src/client/components/itemledger/ElementsItemledger.js +0 -38
  290. package/src/client/components/underpost/CommonUnderpost.js +0 -29
  291. package/src/client/components/underpost/ElementsUnderpost.js +0 -38
  292. package/src/ws/core/management/core.ws.chat.js +0 -8
  293. package/src/ws/core/management/core.ws.mailer.js +0 -16
  294. package/src/ws/core/management/core.ws.stream.js +0 -8
  295. package/src/ws/default/management/default.ws.main.js +0 -8
  296. package/white-paper.md +0 -581
package/src/cli/db.js CHANGED
@@ -6,13 +6,14 @@
6
6
  * Supports MariaDB and MongoDB with import/export capabilities, Git integration, and multi-pod operations.
7
7
  */
8
8
 
9
- import { mergeFile, splitFileFactory } from '../server/conf.js';
9
+ import { mergeFile, splitFileFactory, loadConfServerJson, resolveConfSecrets } from '../server/conf.js';
10
10
  import { loggerFactory } from '../server/logger.js';
11
11
  import { shellExec } from '../server/process.js';
12
12
  import fs from 'fs-extra';
13
13
  import { DataBaseProvider } from '../db/DataBaseProvider.js';
14
- import { loadReplicas, pathPortAssignmentFactory } from '../server/conf.js';
14
+ import { loadReplicas, pathPortAssignmentFactory, loadCronDeployEnv } from '../server/conf.js';
15
15
  import Underpost from '../index.js';
16
+ import { timer } from '../client/components/core/CommonJs.js';
16
17
  const logger = loggerFactory(import.meta);
17
18
 
18
19
  /**
@@ -84,244 +85,18 @@ class UnderpostDB {
84
85
  */
85
86
  static API = {
86
87
  /**
87
- * Helper: Gets filtered pods based on criteria.
88
- * @method _getFilteredPods
88
+ * Helper: Resolves the latest backup timestamp from an existing backup directory.
89
+ * Scans the directory for numeric (epoch) sub-folders and returns the most recent one.
90
+ * @method _getLatestBackupTimestamp
89
91
  * @memberof UnderpostDB
90
- * @param {Object} criteria - Filter criteria.
91
- * @param {string} [criteria.podNames] - Comma-separated pod name patterns.
92
- * @param {string} [criteria.namespace='default'] - Kubernetes namespace.
93
- * @param {string} [criteria.deployId] - Deployment ID pattern.
94
- * @return {Array<PodInfo>} Filtered pod list.
92
+ * @param {string} backupDir - Path to the host-folder backup directory.
93
+ * @return {string|null} The latest timestamp string, or null if none found.
95
94
  */
96
- _getFilteredPods(criteria = {}) {
97
- const { podNames, namespace = 'default', deployId } = criteria;
98
-
99
- try {
100
- // Get all pods using Underpost.deploy.get
101
- let pods = Underpost.deploy.get(deployId || '', 'pods', namespace);
102
-
103
- // Filter by pod names if specified
104
- if (podNames) {
105
- const patterns = podNames.split(',').map((p) => p.trim());
106
- pods = pods.filter((pod) => {
107
- return patterns.some((pattern) => {
108
- // Support wildcards
109
- const regex = new RegExp('^' + pattern.replace(/\*/g, '.*') + '$');
110
- return regex.test(pod.NAME);
111
- });
112
- });
113
- }
114
-
115
- logger.info(`Found ${pods.length} pod(s) matching criteria`, { criteria, podNames: pods.map((p) => p.NAME) });
116
- return pods;
117
- } catch (error) {
118
- logger.error('Error filtering pods', { error: error.message, criteria });
119
- return [];
120
- }
121
- },
122
-
123
- /**
124
- * Helper: Executes kubectl command with error handling.
125
- * @method _executeKubectl
126
- * @memberof UnderpostDB
127
- * @param {string} command - kubectl command to execute.
128
- * @param {Object} [options={}] - Execution options.
129
- * @param {string} [options.context=''] - Command context for logging.
130
- * @return {string|null} Command output or null on error.
131
- */
132
- _executeKubectl(command, options = {}) {
133
- const { context = '' } = options;
134
-
135
- try {
136
- logger.info(`Executing kubectl command`, { command, context });
137
- return shellExec(command, { stdout: true });
138
- } catch (error) {
139
- logger.error(`kubectl command failed`, { command, error: error.message, context });
140
- throw error;
141
- }
142
- },
143
-
144
- /**
145
- * Helper: Copies file to pod.
146
- * @method _copyToPod
147
- * @memberof UnderpostDB
148
- * @param {Object} params - Copy parameters.
149
- * @param {string} params.sourcePath - Source file path.
150
- * @param {string} params.podName - Target pod name.
151
- * @param {string} params.namespace - Pod namespace.
152
- * @param {string} params.destPath - Destination path in pod.
153
- * @return {boolean} Success status.
154
- */
155
- _copyToPod({ sourcePath, podName, namespace, destPath }) {
156
- try {
157
- const command = `sudo kubectl cp ${sourcePath} ${namespace}/${podName}:${destPath}`;
158
- Underpost.db._executeKubectl(command, { context: `copy to pod ${podName}` });
159
- return true;
160
- } catch (error) {
161
- logger.error('Failed to copy file to pod', { sourcePath, podName, destPath, error: error.message });
162
- return false;
163
- }
164
- },
165
-
166
- /**
167
- * Helper: Copies file from pod.
168
- * @method _copyFromPod
169
- * @memberof UnderpostDB
170
- * @param {Object} params - Copy parameters.
171
- * @param {string} params.podName - Source pod name.
172
- * @param {string} params.namespace - Pod namespace.
173
- * @param {string} params.sourcePath - Source path in pod.
174
- * @param {string} params.destPath - Destination file path.
175
- * @return {boolean} Success status.
176
- */
177
- _copyFromPod({ podName, namespace, sourcePath, destPath }) {
178
- try {
179
- const command = `sudo kubectl cp ${namespace}/${podName}:${sourcePath} ${destPath}`;
180
- Underpost.db._executeKubectl(command, { context: `copy from pod ${podName}` });
181
- return true;
182
- } catch (error) {
183
- logger.error('Failed to copy file from pod', { podName, sourcePath, destPath, error: error.message });
184
- return false;
185
- }
186
- },
187
-
188
- /**
189
- * Helper: Executes command in pod.
190
- * @method _execInPod
191
- * @memberof UnderpostDB
192
- * @param {Object} params - Execution parameters.
193
- * @param {string} params.podName - Pod name.
194
- * @param {string} params.namespace - Pod namespace.
195
- * @param {string} params.command - Command to execute.
196
- * @return {string|null} Command output or null.
197
- */
198
- _execInPod({ podName, namespace, command }) {
199
- try {
200
- const kubectlCmd = `sudo kubectl exec -n ${namespace} -i ${podName} -- sh -c "${command}"`;
201
- return Underpost.db._executeKubectl(kubectlCmd, { context: `exec in pod ${podName}` });
202
- } catch (error) {
203
- logger.error('Failed to execute command in pod', { podName, command, error: error.message });
204
- throw error;
205
- }
206
- },
207
-
208
- /**
209
- * Helper: Manages Git repository for backups.
210
- * @method _manageGitRepo
211
- * @memberof UnderpostDB
212
- * @param {Object} params - Git parameters.
213
- * @param {string} params.repoName - Repository name.
214
- * @param {string} params.operation - Operation (clone, pull, commit, push).
215
- * @param {string} [params.message=''] - Commit message.
216
- * @param {boolean} [params.forceClone=false] - Force remove and re-clone repository.
217
- * @return {boolean} Success status.
218
- */
219
- _manageGitRepo({ repoName, operation, message = '', forceClone = false }) {
220
- try {
221
- const username = process.env.GITHUB_USERNAME;
222
- if (!username) {
223
- logger.error('GITHUB_USERNAME environment variable not set');
224
- return false;
225
- }
226
-
227
- const repoPath = `../${repoName}`;
228
-
229
- switch (operation) {
230
- case 'clone':
231
- if (forceClone && fs.existsSync(repoPath)) {
232
- logger.info(`Force clone enabled, removing existing repository: ${repoName}`);
233
- fs.removeSync(repoPath);
234
- }
235
- if (!fs.existsSync(repoPath)) {
236
- shellExec(`cd .. && underpost clone ${username}/${repoName}`);
237
- logger.info(`Cloned repository: ${repoName}`);
238
- }
239
- break;
240
-
241
- case 'pull':
242
- if (fs.existsSync(repoPath)) {
243
- shellExec(`cd ${repoPath} && git checkout . && git clean -f -d`);
244
- shellExec(`cd ${repoPath} && underpost pull . ${username}/${repoName}`, {
245
- silent: true,
246
- });
247
- logger.info(`Pulled repository: ${repoName}`);
248
- }
249
- break;
250
-
251
- case 'commit':
252
- if (fs.existsSync(repoPath)) {
253
- shellExec(`cd ${repoPath} && git add .`);
254
- shellExec(`underpost cmt ${repoPath} backup '' '${message}'`);
255
- logger.info(`Committed to repository: ${repoName}`, { message });
256
- }
257
- break;
258
-
259
- case 'push':
260
- if (fs.existsSync(repoPath)) {
261
- shellExec(`cd ${repoPath} && underpost push . ${username}/${repoName}`, { silent: true });
262
- logger.info(`Pushed repository: ${repoName}`);
263
- }
264
- break;
265
-
266
- default:
267
- logger.warn(`Unknown git operation: ${operation}`);
268
- return false;
269
- }
270
-
271
- return true;
272
- } catch (error) {
273
- logger.error(`Git operation failed`, { repoName, operation, error: error.message });
274
- return false;
275
- }
276
- },
277
-
278
- /**
279
- * Helper: Manages backup timestamps and cleanup.
280
- * @method _manageBackupTimestamps
281
- * @memberof UnderpostDB
282
- * @param {string} backupPath - Backup directory path.
283
- * @param {number} newTimestamp - New backup timestamp.
284
- * @param {boolean} shouldCleanup - Whether to cleanup old backups.
285
- * @return {Object} Backup info with current and removed timestamps.
286
- */
287
- _manageBackupTimestamps(backupPath, newTimestamp, shouldCleanup) {
288
- try {
289
- if (!fs.existsSync(backupPath)) {
290
- fs.mkdirSync(backupPath, { recursive: true });
291
- }
292
-
293
- // Delete empty folders
294
- shellExec(`cd ${backupPath} && find . -type d -empty -delete`);
295
-
296
- const times = fs.readdirSync(backupPath);
297
- const validTimes = times.map((t) => parseInt(t)).filter((t) => !isNaN(t));
298
-
299
- const currentBackupTimestamp = validTimes.length > 0 ? Math.max(...validTimes) : null;
300
- const removeBackupTimestamp = validTimes.length > 0 ? Math.min(...validTimes) : null;
301
-
302
- // Cleanup old backups if we have too many
303
- if (shouldCleanup && validTimes.length >= MAX_BACKUP_RETENTION && removeBackupTimestamp) {
304
- const removeDir = `${backupPath}/${removeBackupTimestamp}`;
305
- logger.info('Removing old backup', { path: removeDir });
306
- fs.removeSync(removeDir);
307
- }
308
-
309
- // Create new backup directory
310
- if (shouldCleanup) {
311
- const newBackupDir = `${backupPath}/${newTimestamp}`;
312
- logger.info('Creating new backup directory', { path: newBackupDir });
313
- fs.mkdirSync(newBackupDir, { recursive: true });
314
- }
315
-
316
- return {
317
- current: currentBackupTimestamp,
318
- removed: removeBackupTimestamp,
319
- count: validTimes.length,
320
- };
321
- } catch (error) {
322
- logger.error('Error managing backup timestamps', { backupPath, error: error.message });
323
- return { current: null, removed: null, count: 0 };
324
- }
95
+ _getLatestBackupTimestamp(backupDir) {
96
+ if (!fs.existsSync(backupDir)) return null;
97
+ const entries = fs.readdirSync(backupDir).filter((e) => /^\d+$/.test(e));
98
+ if (entries.length === 0) return null;
99
+ return entries.sort((a, b) => parseInt(b) - parseInt(a))[0];
325
100
  },
326
101
 
327
102
  /**
@@ -344,8 +119,20 @@ class UnderpostDB {
344
119
 
345
120
  logger.info('Importing MariaDB database', { podName, dbName });
346
121
 
122
+ // Always ensure the database exists first — required for WP even when no backup is available
123
+ Underpost.kubectl.run(
124
+ `kubectl exec -n ${namespace} -i ${podName} -- mariadb -p${password} -e 'CREATE DATABASE IF NOT EXISTS ${dbName};'`,
125
+ { context: `create database ${dbName}` },
126
+ );
127
+
128
+ // If no SQL file is available, the empty database is enough — return early
129
+ if (!sqlPath || !fs.existsSync(sqlPath)) {
130
+ logger.warn('No SQL backup file found — empty database ensured', { podName, dbName, sqlPath });
131
+ return true;
132
+ }
133
+
347
134
  // Remove existing SQL file in container
348
- Underpost.db._execInPod({
135
+ Underpost.kubectl.exec({
349
136
  podName,
350
137
  namespace,
351
138
  command: `rm -rf ${containerSqlPath}`,
@@ -353,7 +140,7 @@ class UnderpostDB {
353
140
 
354
141
  // Copy SQL file to pod
355
142
  if (
356
- !Underpost.db._copyToPod({
143
+ !Underpost.kubectl.cpTo({
357
144
  sourcePath: sqlPath,
358
145
  podName,
359
146
  namespace,
@@ -363,15 +150,9 @@ class UnderpostDB {
363
150
  return false;
364
151
  }
365
152
 
366
- // Create database if it doesn't exist
367
- Underpost.db._executeKubectl(
368
- `kubectl exec -n ${namespace} -i ${podName} -- mariadb -p${password} -e 'CREATE DATABASE IF NOT EXISTS ${dbName};'`,
369
- { context: `create database ${dbName}` },
370
- );
371
-
372
153
  // Import SQL file
373
154
  const importCmd = `mariadb -u ${user} -p${password} ${dbName} < ${containerSqlPath}`;
374
- Underpost.db._execInPod({ podName, namespace, command: importCmd });
155
+ Underpost.kubectl.exec({ podName, namespace, command: importCmd });
375
156
 
376
157
  logger.info('Successfully imported MariaDB database', { podName, dbName });
377
158
  return true;
@@ -402,7 +183,7 @@ class UnderpostDB {
402
183
  logger.info('Exporting MariaDB database', { podName, dbName });
403
184
 
404
185
  // Remove existing SQL file in container
405
- Underpost.db._execInPod({
186
+ Underpost.kubectl.exec({
406
187
  podName,
407
188
  namespace,
408
189
  command: `rm -rf ${containerSqlPath}`,
@@ -410,11 +191,11 @@ class UnderpostDB {
410
191
 
411
192
  // Dump database
412
193
  const dumpCmd = `mariadb-dump --user=${user} --password=${password} --lock-tables ${dbName} > ${containerSqlPath}`;
413
- Underpost.db._execInPod({ podName, namespace, command: dumpCmd });
194
+ Underpost.kubectl.exec({ podName, namespace, command: dumpCmd });
414
195
 
415
196
  // Copy SQL file from pod
416
197
  if (
417
- !Underpost.db._copyFromPod({
198
+ !Underpost.kubectl.cpFrom({
418
199
  podName,
419
200
  namespace,
420
201
  sourcePath: containerSqlPath,
@@ -457,8 +238,18 @@ class UnderpostDB {
457
238
 
458
239
  logger.info('Importing MongoDB database', { podName, dbName });
459
240
 
241
+ // If no BSON directory is available, skip — MongoDB creates the DB on first write
242
+ if (!bsonPath || !fs.existsSync(bsonPath)) {
243
+ logger.warn('No BSON backup directory found — database will be created on first write', {
244
+ podName,
245
+ dbName,
246
+ bsonPath,
247
+ });
248
+ return true;
249
+ }
250
+
460
251
  // Remove existing BSON directory in container
461
- Underpost.db._execInPod({
252
+ Underpost.kubectl.exec({
462
253
  podName,
463
254
  namespace,
464
255
  command: `rm -rf ${containerBsonPath}`,
@@ -466,7 +257,7 @@ class UnderpostDB {
466
257
 
467
258
  // Copy BSON directory to pod
468
259
  if (
469
- !Underpost.db._copyToPod({
260
+ !Underpost.kubectl.cpTo({
470
261
  sourcePath: bsonPath,
471
262
  podName,
472
263
  namespace,
@@ -480,7 +271,7 @@ class UnderpostDB {
480
271
  const restoreCmd = `mongorestore -d ${dbName} ${containerBsonPath}${drop ? ' --drop' : ''}${
481
272
  preserveUUID ? ' --preserveUUID' : ''
482
273
  }`;
483
- Underpost.db._execInPod({ podName, namespace, command: restoreCmd });
274
+ Underpost.kubectl.exec({ podName, namespace, command: restoreCmd });
484
275
 
485
276
  logger.info('Successfully imported MongoDB database', { podName, dbName });
486
277
  return true;
@@ -510,7 +301,7 @@ class UnderpostDB {
510
301
  logger.info('Exporting MongoDB database', { podName, dbName, collections });
511
302
 
512
303
  // Remove existing BSON directory in container
513
- Underpost.db._execInPod({
304
+ Underpost.kubectl.exec({
514
305
  podName,
515
306
  namespace,
516
307
  command: `rm -rf ${containerBsonPath}`,
@@ -521,16 +312,16 @@ class UnderpostDB {
521
312
  const collectionList = collections.split(',').map((c) => c.trim());
522
313
  for (const collection of collectionList) {
523
314
  const dumpCmd = `mongodump -d ${dbName} --collection ${collection} -o /`;
524
- Underpost.db._execInPod({ podName, namespace, command: dumpCmd });
315
+ Underpost.kubectl.exec({ podName, namespace, command: dumpCmd });
525
316
  }
526
317
  } else {
527
318
  const dumpCmd = `mongodump -d ${dbName} -o /`;
528
- Underpost.db._execInPod({ podName, namespace, command: dumpCmd });
319
+ Underpost.kubectl.exec({ podName, namespace, command: dumpCmd });
529
320
  }
530
321
 
531
322
  // Copy BSON directory from pod
532
323
  if (
533
- !Underpost.db._copyFromPod({
324
+ !Underpost.kubectl.cpFrom({
534
325
  podName,
535
326
  namespace,
536
327
  sourcePath: containerBsonPath,
@@ -624,7 +415,7 @@ class UnderpostDB {
624
415
  logger.info('Getting MariaDB table statistics', { podName, dbName });
625
416
 
626
417
  const command = `sudo kubectl exec -n ${namespace} -i ${podName} -- mariadb -u ${user} -p${password} ${dbName} -e "SELECT TABLE_NAME as 'table', TABLE_ROWS as 'count' FROM information_schema.TABLES WHERE TABLE_SCHEMA = '${dbName}' ORDER BY TABLE_NAME;" --skip-column-names --batch`;
627
- const output = shellExec(command, { stdout: true, silent: true });
418
+ const output = shellExec(command, { stdout: true, silent: true, disableLog: true });
628
419
 
629
420
  if (!output || output.trim() === '') {
630
421
  logger.warn('No tables found or empty output');
@@ -758,6 +549,7 @@ class UnderpostDB {
758
549
  * @param {boolean} [options.k3s=false] - k3s cluster flag.
759
550
  * @param {boolean} [options.kubeadm=false] - kubeadm cluster flag.
760
551
  * @param {boolean} [options.kind=false] - kind cluster flag.
552
+ * @param {boolean} [options.repoBackup=false] - Backs up repositories (git commit+push) inside deployment pods via kubectl exec.
761
553
  * @return {Promise<void>} Resolves when operation is complete.
762
554
  */
763
555
  async callback(
@@ -786,350 +578,375 @@ class UnderpostDB {
786
578
  k3s: false,
787
579
  kubeadm: false,
788
580
  kind: false,
581
+ repoBackup: false,
789
582
  },
790
583
  ) {
791
- const newBackupTimestamp = new Date().getTime();
792
- const namespace = options.ns && typeof options.ns === 'string' ? options.ns : 'default';
793
-
794
- if (deployList === 'dd') deployList = fs.readFileSync(`./engine-private/deploy/dd.router`, 'utf8');
795
-
796
- // Handle clean-fs-collection operation
797
- if (options.cleanFsCollection || options.cleanFsDryRun) {
798
- logger.info('Starting File collection cleanup operation', { deployList });
799
- await Underpost.db.cleanFsCollection(deployList, {
800
- hosts: options.hosts,
801
- paths: options.paths,
802
- dryRun: options.cleanFsDryRun,
803
- });
804
- return;
805
- }
584
+ // Ensure engine-private is available (clone if inside a deployment
585
+ // container where globalSecretClean has already removed it).
586
+ const firstDeployId = deployList !== 'dd' ? deployList.split(',')[0].trim() : '';
587
+ try {
588
+ loadCronDeployEnv();
589
+ const newBackupTimestamp = new Date().getTime();
590
+ const namespace = options.ns && typeof options.ns === 'string' ? options.ns : 'default';
591
+
592
+ if (deployList === 'dd') deployList = fs.readFileSync(`./engine-private/deploy/dd.router`, 'utf8');
593
+
594
+ // Handle repository backup (git commit+push inside deployment pod)
595
+ if (options.repoBackup) {
596
+ const namespace = options.ns && typeof options.ns === 'string' ? options.ns : 'default';
597
+ for (const _deployId of deployList.split(',')) {
598
+ const deployId = _deployId.trim();
599
+ if (!deployId) continue;
600
+ logger.info('Starting pod repository backup', { deployId, namespace });
601
+ Underpost.repo.backupPodRepositories({
602
+ deployId,
603
+ namespace,
604
+ env: options.dev ? 'development' : 'production',
605
+ });
606
+ }
607
+ return;
608
+ }
806
609
 
807
- logger.info('Starting database operation', {
808
- deployList,
809
- namespace,
810
- import: options.import,
811
- export: options.export,
812
- });
610
+ // Handle clean-fs-collection operation
611
+ if (options.cleanFsCollection || options.cleanFsDryRun) {
612
+ logger.info('Starting File collection cleanup operation', { deployList });
613
+ await Underpost.db.cleanFsCollection(deployList, {
614
+ hosts: options.hosts,
615
+ paths: options.paths,
616
+ dryRun: options.cleanFsDryRun,
617
+ });
618
+ return;
619
+ }
620
+
621
+ logger.info('Starting database operation', {
622
+ deployList,
623
+ namespace,
624
+ import: options.import,
625
+ export: options.export,
626
+ });
813
627
 
814
- if (options.primaryPodEnsure) {
815
- const primaryPodName = Underpost.db.getMongoPrimaryPodName({ namespace, podName: options.primaryPodEnsure });
816
- if (!primaryPodName) {
817
- const baseCommand = options.dev ? 'node bin' : 'underpost';
818
- const baseClusterCommand = options.dev ? ' --dev' : '';
819
- let clusterFlag = options.k3s ? ' --k3s' : options.kubeadm ? ' --kubeadm' : '';
820
- shellExec(`${baseCommand} cluster${baseClusterCommand}${clusterFlag} --mongodb`);
628
+ if (options.primaryPodEnsure) {
629
+ const primaryPodName = Underpost.db.getMongoPrimaryPodName({ namespace, podName: options.primaryPodEnsure });
630
+ if (!primaryPodName) {
631
+ const baseCommand = options.dev ? 'node bin' : 'underpost';
632
+ const baseClusterCommand = options.dev ? ' --dev' : '';
633
+ let clusterFlag = options.k3s ? ' --k3s' : options.kubeadm ? ' --kubeadm' : '';
634
+ shellExec(`${baseCommand} cluster${baseClusterCommand}${clusterFlag} --mongodb`);
635
+ }
636
+ return;
821
637
  }
822
- return;
823
- }
824
638
 
825
- // Track processed repositories to avoid duplicate Git operations
826
- const processedRepos = new Set();
827
- // Track processed host+path combinations to avoid duplicates
828
- const processedHostPaths = new Set();
639
+ // Track processed repositories to avoid duplicate Git operations
640
+ const processedRepos = new Set();
641
+ // Track processed host+path combinations to avoid duplicates
642
+ const processedHostPaths = new Set();
829
643
 
830
- for (const _deployId of deployList.split(',')) {
831
- const deployId = _deployId.trim();
832
- if (!deployId) continue;
644
+ for (const _deployId of deployList.split(',')) {
645
+ const deployId = _deployId.trim();
646
+ if (!deployId) continue;
833
647
 
834
- logger.info('Processing deployment', { deployId });
648
+ logger.info('Processing deployment', { deployId });
835
649
 
836
- /** @type {Object.<string, Object.<string, DatabaseConfig>>} */
837
- const dbs = {};
838
- const repoName = `engine-${deployId.includes('dd-') ? deployId.split('dd-')[1] : deployId}-cron-backups`;
650
+ /** @type {Object.<string, Object.<string, DatabaseConfig>>} */
651
+ const dbs = {};
652
+ const repoName = `engine-${deployId.includes('dd-') ? deployId.split('dd-')[1] : deployId}-cron-backups`;
839
653
 
840
- // Load server configuration
841
- const confServerPath = `./engine-private/conf/${deployId}/conf.server.json`;
842
- if (!fs.existsSync(confServerPath)) {
843
- logger.error('Configuration file not found', { path: confServerPath });
844
- continue;
845
- }
654
+ // Load server configuration
655
+ const confServerPath = `./engine-private/conf/${deployId}/conf.server.json`;
656
+ if (!fs.existsSync(confServerPath)) {
657
+ logger.error('Configuration file not found', { path: confServerPath });
658
+ continue;
659
+ }
846
660
 
847
- const confServer = JSON.parse(fs.readFileSync(confServerPath, 'utf8'));
848
-
849
- // Build database configuration map
850
- for (const host of Object.keys(confServer)) {
851
- for (const path of Object.keys(confServer[host])) {
852
- const { db } = confServer[host][path];
853
- if (db) {
854
- const { provider, name, user, password } = db;
855
- if (!dbs[provider]) dbs[provider] = {};
856
-
857
- if (!(name in dbs[provider])) {
858
- dbs[provider][name] = {
859
- user,
860
- password,
861
- hostFolder: host + path.replaceAll('/', '-'),
862
- host,
863
- path,
864
- };
661
+ const confServer = loadConfServerJson(confServerPath, { resolve: true });
662
+
663
+ // Build database configuration map
664
+ for (const host of Object.keys(confServer)) {
665
+ for (const path of Object.keys(confServer[host])) {
666
+ const { db } = confServer[host][path];
667
+ if (db) {
668
+ const { provider, name, user, password } = db;
669
+ if (!dbs[provider]) dbs[provider] = {};
670
+
671
+ if (!(name in dbs[provider])) {
672
+ dbs[provider][name] = {
673
+ user,
674
+ password,
675
+ hostFolder: host + path.replaceAll('/', '-'),
676
+ host,
677
+ path,
678
+ };
679
+ }
865
680
  }
866
681
  }
867
682
  }
868
- }
869
-
870
- // Handle Git operations - execute only once per repository
871
- if (!processedRepos.has(repoName)) {
872
- logger.info('Processing Git operations for repository', { repoName, deployId });
873
- if (options.git === true) {
874
- Underpost.db._manageGitRepo({ repoName, operation: 'clone', forceClone: options.forceClone });
875
- Underpost.db._manageGitRepo({ repoName, operation: 'pull' });
876
- }
877
683
 
878
- if (options.macroRollbackExport) {
879
- // Only clone if not already done by git option above
880
- if (options.git !== true) {
881
- Underpost.db._manageGitRepo({ repoName, operation: 'clone', forceClone: options.forceClone });
882
- Underpost.db._manageGitRepo({ repoName, operation: 'pull' });
684
+ // Handle Git operations - execute only once per repository
685
+ if (!processedRepos.has(repoName)) {
686
+ logger.info('Processing Git operations for repository', { repoName, deployId });
687
+ if (options.git === true) {
688
+ Underpost.repo.manageBackupRepo({ repoName, operation: 'clone', forceClone: options.forceClone });
689
+ Underpost.repo.manageBackupRepo({ repoName, operation: 'pull' });
883
690
  }
884
691
 
885
- const nCommits = parseInt(options.macroRollbackExport);
886
- const repoPath = `../${repoName}`;
887
- const username = process.env.GITHUB_USERNAME;
888
-
889
- if (fs.existsSync(repoPath) && username) {
890
- logger.info('Executing macro rollback export', { repoName, nCommits });
891
- shellExec(`cd ${repoPath} && underpost cmt . reset ${nCommits}`);
892
- shellExec(`cd ${repoPath} && git reset`);
893
- shellExec(`cd ${repoPath} && git checkout .`);
894
- shellExec(`cd ${repoPath} && git clean -f -d`);
895
- shellExec(`cd ${repoPath} && underpost push . ${username}/${repoName} -f`);
896
- } else {
897
- if (!username) logger.error('GITHUB_USERNAME environment variable not set');
898
- logger.warn('Repository not found for macro rollback', { repoPath });
899
- }
900
- }
692
+ if (options.macroRollbackExport) {
693
+ // Only clone if not already done by git option above
694
+ if (options.git !== true) {
695
+ Underpost.repo.manageBackupRepo({ repoName, operation: 'clone', forceClone: options.forceClone });
696
+ Underpost.repo.manageBackupRepo({ repoName, operation: 'pull' });
697
+ }
901
698
 
902
- processedRepos.add(repoName);
903
- logger.info('Repository marked as processed', { repoName });
904
- } else {
905
- logger.info('Skipping Git operations for already processed repository', { repoName, deployId });
906
- }
699
+ const nCommits = parseInt(options.macroRollbackExport);
700
+ const repoPath = `../${repoName}`;
701
+ const username = process.env.GITHUB_USERNAME;
702
+
703
+ if (fs.existsSync(repoPath) && username) {
704
+ logger.info('Executing macro rollback export', { repoName, nCommits });
705
+ shellExec(`cd ${repoPath} && underpost cmt . reset ${nCommits}`);
706
+ shellExec(`cd ${repoPath} && git reset`);
707
+ shellExec(`cd ${repoPath} && git checkout .`);
708
+ shellExec(`cd ${repoPath} && git clean -f -d`);
709
+ shellExec(`cd ${repoPath} && underpost push . ${username}/${repoName} -f`);
710
+ } else {
711
+ if (!username) logger.error('GITHUB_USERNAME environment variable not set');
712
+ logger.warn('Repository not found for macro rollback', { repoPath });
713
+ }
714
+ }
907
715
 
908
- // Process each database provider
909
- for (const provider of Object.keys(dbs)) {
910
- for (const dbName of Object.keys(dbs[provider])) {
911
- const { hostFolder, user, password, host, path } = dbs[provider][dbName];
716
+ processedRepos.add(repoName);
717
+ logger.info('Repository marked as processed', { repoName });
718
+ } else {
719
+ logger.info('Skipping Git operations for already processed repository', { repoName, deployId });
720
+ }
912
721
 
913
- // Create unique identifier for host+path combination
914
- const hostPathKey = `${deployId}:${host}:${path}`;
722
+ // Process each database provider
723
+ for (const provider of Object.keys(dbs)) {
724
+ for (const dbName of Object.keys(dbs[provider])) {
725
+ const { hostFolder, user, password, host, path } = dbs[provider][dbName];
915
726
 
916
- // Skip if this host+path combination was already processed
917
- if (processedHostPaths.has(hostPathKey)) {
918
- logger.info('Skipping already processed host/path', { dbName, host, path, deployId });
919
- continue;
920
- }
727
+ // Create unique identifier for host+path combination
728
+ const hostPathKey = `${deployId}:${host}:${path}`;
921
729
 
922
- // Filter by hosts and paths if specified
923
- if (
924
- (options.hosts &&
925
- !options.hosts
926
- .split(',')
927
- .map((h) => h.trim())
928
- .includes(host)) ||
929
- (options.paths &&
930
- !options.paths
931
- .split(',')
932
- .map((p) => p.trim())
933
- .includes(path))
934
- ) {
935
- logger.info('Skipping database due to host/path filter', { dbName, host, path });
936
- continue;
937
- }
730
+ // Skip if this host+path combination was already processed
731
+ if (processedHostPaths.has(hostPathKey)) {
732
+ logger.info('Skipping already processed host/path', { dbName, host, path, deployId });
733
+ continue;
734
+ }
938
735
 
939
- if (!hostFolder) {
940
- logger.warn('No hostFolder defined for database', { dbName, provider });
941
- continue;
942
- }
736
+ // Filter by hosts and paths if specified
737
+ if (
738
+ (options.hosts &&
739
+ !options.hosts
740
+ .split(',')
741
+ .map((h) => h.trim())
742
+ .includes(host)) ||
743
+ (options.paths &&
744
+ !options.paths
745
+ .split(',')
746
+ .map((p) => p.trim())
747
+ .includes(path))
748
+ ) {
749
+ logger.info('Skipping database due to host/path filter', { dbName, host, path });
750
+ continue;
751
+ }
943
752
 
944
- logger.info('Processing database', { hostFolder, provider, dbName, deployId });
753
+ if (!hostFolder) {
754
+ logger.warn('No hostFolder defined for database', { dbName, provider });
755
+ continue;
756
+ }
945
757
 
946
- const backUpPath = `../${repoName}/${hostFolder}`;
947
- const backupInfo = Underpost.db._manageBackupTimestamps(
948
- backUpPath,
949
- newBackupTimestamp,
950
- options.export === true,
951
- );
758
+ logger.info('Processing database', { hostFolder, provider, dbName, deployId });
952
759
 
953
- dbs[provider][dbName].currentBackupTimestamp = backupInfo.current;
760
+ const latestBackupTimestamp = Underpost.db._getLatestBackupTimestamp(`../${repoName}/${hostFolder}`);
954
761
 
955
- const currentTimestamp = backupInfo.current || newBackupTimestamp;
956
- const sqlContainerPath = `/home/${dbName}.sql`;
957
- const fromPartsPath = `../${repoName}/${hostFolder}/${currentTimestamp}/${dbName}-parths.json`;
958
- const toSqlPath = `../${repoName}/${hostFolder}/${currentTimestamp}/${dbName}.sql`;
959
- const toNewSqlPath = `../${repoName}/${hostFolder}/${newBackupTimestamp}/${dbName}.sql`;
960
- const toBsonPath = `../${repoName}/${hostFolder}/${currentTimestamp}/${dbName}`;
961
- const toNewBsonPath = `../${repoName}/${hostFolder}/${newBackupTimestamp}/${dbName}`;
762
+ dbs[provider][dbName].currentBackupTimestamp = latestBackupTimestamp;
962
763
 
963
- // Merge split SQL files if needed for import
964
- if (options.import === true && fs.existsSync(fromPartsPath) && !fs.existsSync(toSqlPath)) {
965
- const names = JSON.parse(fs.readFileSync(fromPartsPath, 'utf8')).map((_path) => {
966
- return `../${repoName}/${hostFolder}/${currentTimestamp}/${_path.split('/').pop()}`;
967
- });
968
- logger.info('Merging backup parts', { fromPartsPath, toSqlPath, parts: names.length });
969
- await mergeFile(names, toSqlPath);
970
- }
764
+ const currentTimestamp = latestBackupTimestamp || newBackupTimestamp;
765
+ const sqlContainerPath = `/home/${dbName}.sql`;
766
+ const fromPartsPath = `../${repoName}/${hostFolder}/${currentTimestamp}/${dbName}-parths.json`;
767
+ const toSqlPath = `../${repoName}/${hostFolder}/${currentTimestamp}/${dbName}.sql`;
768
+ const toNewSqlPath = `../${repoName}/${hostFolder}/${newBackupTimestamp}/${dbName}.sql`;
769
+ const toBsonPath = `../${repoName}/${hostFolder}/${currentTimestamp}/${dbName}`;
770
+ const toNewBsonPath = `../${repoName}/${hostFolder}/${newBackupTimestamp}/${dbName}`;
971
771
 
972
- // Get target pods based on provider and options
973
- let targetPods = [];
974
- const podCriteria = {
975
- podNames: options.podName,
976
- namespace,
977
- deployId: provider === 'mariadb' ? 'mariadb' : 'mongo',
978
- };
772
+ // Merge split SQL files if needed for import
773
+ if (options.import === true && fs.existsSync(fromPartsPath) && !fs.existsSync(toSqlPath)) {
774
+ const names = JSON.parse(fs.readFileSync(fromPartsPath, 'utf8')).map((_path) => {
775
+ return `../${repoName}/${hostFolder}/${currentTimestamp}/${_path.split('/').pop()}`;
776
+ });
777
+ logger.info('Merging backup parts', { fromPartsPath, toSqlPath, parts: names.length });
778
+ await mergeFile(names, toSqlPath);
779
+ }
979
780
 
980
- targetPods = Underpost.db._getFilteredPods(podCriteria);
781
+ // Get target pods based on provider and options
782
+ let targetPods = [];
783
+ const podCriteria = {
784
+ podNames: options.podName,
785
+ namespace,
786
+ deployId: provider === 'mariadb' ? 'mariadb' : 'mongo',
787
+ };
981
788
 
982
- // Fallback to default if no custom pods specified
983
- if (targetPods.length === 0 && !options.podName) {
984
- const defaultPods = Underpost.deploy.get(provider === 'mariadb' ? 'mariadb' : 'mongo', 'pods', namespace);
985
- console.log('defaultPods', defaultPods);
986
- targetPods = defaultPods;
987
- }
789
+ targetPods = Underpost.kubectl.getFilteredPods(podCriteria);
790
+
791
+ // Fallback to default if no custom pods specified
792
+ if (targetPods.length === 0 && !options.podName) {
793
+ const defaultPods = Underpost.kubectl.get(
794
+ provider === 'mariadb' ? 'mariadb' : 'mongo',
795
+ 'pods',
796
+ namespace,
797
+ );
798
+ console.log('defaultPods', defaultPods);
799
+ targetPods = defaultPods;
800
+ }
988
801
 
989
- if (targetPods.length === 0) {
990
- logger.warn('No pods found matching criteria', { provider, criteria: podCriteria });
991
- continue;
992
- }
802
+ if (targetPods.length === 0) {
803
+ logger.warn('No pods found matching criteria', { provider, criteria: podCriteria });
804
+ continue;
805
+ }
993
806
 
994
- // Handle primary pod detection for MongoDB
995
- let podsToProcess = [];
996
- if (provider === 'mongoose' && !options.allPods) {
997
- // For MongoDB, always use primary pod unless allPods is true
998
- if (!targetPods || targetPods.length === 0) {
999
- logger.warn('No MongoDB pods available to check for primary');
1000
- podsToProcess = [];
1001
- } else {
1002
- const firstPod = targetPods[0].NAME;
1003
- const primaryPodName = Underpost.db.getMongoPrimaryPodName({ namespace, podName: firstPod });
1004
-
1005
- if (primaryPodName) {
1006
- const primaryPod = targetPods.find((p) => p.NAME === primaryPodName);
1007
- if (primaryPod) {
1008
- podsToProcess = [primaryPod];
1009
- logger.info('Using MongoDB primary pod', { primaryPod: primaryPodName });
807
+ // Handle primary pod detection for MongoDB
808
+ let podsToProcess = [];
809
+ if (provider === 'mongoose' && !options.allPods) {
810
+ // For MongoDB, always use primary pod unless allPods is true
811
+ if (!targetPods || targetPods.length === 0) {
812
+ logger.warn('No MongoDB pods available to check for primary');
813
+ podsToProcess = [];
814
+ } else {
815
+ const firstPod = targetPods[0].NAME;
816
+ const primaryPodName = Underpost.db.getMongoPrimaryPodName({ namespace, podName: firstPod });
817
+
818
+ if (primaryPodName) {
819
+ const primaryPod = targetPods.find((p) => p.NAME === primaryPodName);
820
+ if (primaryPod) {
821
+ podsToProcess = [primaryPod];
822
+ logger.info('Using MongoDB primary pod', { primaryPod: primaryPodName });
823
+ } else {
824
+ logger.warn('Primary pod not in filtered list, using first pod', { primaryPodName });
825
+ podsToProcess = [targetPods[0]];
826
+ }
1010
827
  } else {
1011
- logger.warn('Primary pod not in filtered list, using first pod', { primaryPodName });
828
+ logger.warn('Could not detect primary pod, using first pod');
1012
829
  podsToProcess = [targetPods[0]];
1013
830
  }
1014
- } else {
1015
- logger.warn('Could not detect primary pod, using first pod');
1016
- podsToProcess = [targetPods[0]];
1017
831
  }
832
+ } else {
833
+ // For MariaDB or when allPods is true, limit to first pod unless allPods is true
834
+ podsToProcess = options.allPods === true ? targetPods : [targetPods[0]];
1018
835
  }
1019
- } else {
1020
- // For MariaDB or when allPods is true, limit to first pod unless allPods is true
1021
- podsToProcess = options.allPods === true ? targetPods : [targetPods[0]];
1022
- }
1023
836
 
1024
- logger.info(`Processing ${podsToProcess.length} pod(s) for ${provider}`, {
1025
- dbName,
1026
- pods: podsToProcess.map((p) => p.NAME),
1027
- });
837
+ logger.info(`Processing ${podsToProcess.length} pod(s) for ${provider}`, {
838
+ dbName,
839
+ pods: podsToProcess.map((p) => p.NAME),
840
+ });
1028
841
 
1029
- // Process each pod
1030
- for (const pod of podsToProcess) {
1031
- logger.info('Processing pod', { podName: pod.NAME, node: pod.NODE, status: pod.STATUS });
1032
-
1033
- switch (provider) {
1034
- case 'mariadb': {
1035
- if (options.stats === true) {
1036
- const stats = Underpost.db._getMariaDBStats({
1037
- podName: pod.NAME,
1038
- namespace,
1039
- dbName,
1040
- user,
1041
- password,
1042
- });
1043
- if (stats) {
1044
- Underpost.db._displayStats({ provider, dbName, stats });
842
+ // Process each pod
843
+ for (const pod of podsToProcess) {
844
+ logger.info('Processing pod', { podName: pod.NAME, node: pod.NODE, status: pod.STATUS });
845
+
846
+ switch (provider) {
847
+ case 'mariadb': {
848
+ if (options.stats === true) {
849
+ const stats = Underpost.db._getMariaDBStats({
850
+ podName: pod.NAME,
851
+ namespace,
852
+ dbName,
853
+ user,
854
+ password,
855
+ });
856
+ if (stats) {
857
+ Underpost.db._displayStats({ provider, dbName, stats });
858
+ }
1045
859
  }
1046
- }
1047
860
 
1048
- if (options.import === true) {
1049
- Underpost.db._importMariaDB({
1050
- pod,
1051
- namespace,
1052
- dbName,
1053
- user,
1054
- password,
1055
- sqlPath: toSqlPath,
1056
- });
1057
- }
861
+ if (options.import === true) {
862
+ Underpost.db._importMariaDB({
863
+ pod,
864
+ namespace,
865
+ dbName,
866
+ user,
867
+ password,
868
+ sqlPath: toSqlPath,
869
+ });
870
+ }
1058
871
 
1059
- if (options.export === true) {
1060
- const outputPath = options.outPath || toNewSqlPath;
1061
- await Underpost.db._exportMariaDB({
1062
- pod,
1063
- namespace,
1064
- dbName,
1065
- user,
1066
- password,
1067
- outputPath,
1068
- });
872
+ if (options.export === true) {
873
+ const outputPath = options.outPath || toNewSqlPath;
874
+ await Underpost.db._exportMariaDB({
875
+ pod,
876
+ namespace,
877
+ dbName,
878
+ user,
879
+ password,
880
+ outputPath,
881
+ });
882
+ }
883
+ break;
1069
884
  }
1070
- break;
1071
- }
1072
885
 
1073
- case 'mongoose': {
1074
- if (options.stats === true) {
1075
- const stats = Underpost.db._getMongoStats({
1076
- podName: pod.NAME,
1077
- namespace,
1078
- dbName,
1079
- });
1080
- if (stats) {
1081
- Underpost.db._displayStats({ provider, dbName, stats });
886
+ case 'mongoose': {
887
+ if (options.stats === true) {
888
+ const stats = Underpost.db._getMongoStats({
889
+ podName: pod.NAME,
890
+ namespace,
891
+ dbName,
892
+ });
893
+ if (stats) {
894
+ Underpost.db._displayStats({ provider, dbName, stats });
895
+ }
1082
896
  }
1083
- }
1084
897
 
1085
- if (options.import === true) {
1086
- const bsonPath = options.outPath || toBsonPath;
1087
- Underpost.db._importMongoDB({
1088
- pod,
1089
- namespace,
1090
- dbName,
1091
- bsonPath,
1092
- drop: options.drop,
1093
- preserveUUID: options.preserveUUID,
1094
- });
1095
- }
898
+ if (options.import === true) {
899
+ const bsonPath = options.outPath || toBsonPath;
900
+ Underpost.db._importMongoDB({
901
+ pod,
902
+ namespace,
903
+ dbName,
904
+ bsonPath,
905
+ drop: options.drop,
906
+ preserveUUID: options.preserveUUID,
907
+ });
908
+ }
1096
909
 
1097
- if (options.export === true) {
1098
- const outputPath = options.outPath || toNewBsonPath;
1099
- Underpost.db._exportMongoDB({
1100
- pod,
1101
- namespace,
1102
- dbName,
1103
- outputPath,
1104
- collections: options.collections,
1105
- });
910
+ if (options.export === true) {
911
+ const outputPath = options.outPath || toNewBsonPath;
912
+ Underpost.db._exportMongoDB({
913
+ pod,
914
+ namespace,
915
+ dbName,
916
+ outputPath,
917
+ collections: options.collections,
918
+ });
919
+ }
920
+ break;
1106
921
  }
1107
- break;
1108
- }
1109
922
 
1110
- default:
1111
- logger.warn('Unsupported database provider', { provider });
1112
- break;
923
+ default:
924
+ logger.warn('Unsupported database provider', { provider });
925
+ break;
926
+ }
1113
927
  }
928
+
929
+ // Mark this host+path combination as processed
930
+ processedHostPaths.add(hostPathKey);
1114
931
  }
932
+ }
1115
933
 
1116
- // Mark this host+path combination as processed
1117
- processedHostPaths.add(hostPathKey);
934
+ // Commit and push to Git if enabled - execute only once per repository
935
+ if (options.export === true && options.git === true && !processedRepos.has(`${repoName}-committed`)) {
936
+ const commitMessage = `${new Date(newBackupTimestamp).toLocaleDateString()} ${new Date(
937
+ newBackupTimestamp,
938
+ ).toLocaleTimeString()}`;
939
+ Underpost.repo.manageBackupRepo({ repoName, operation: 'commit', message: commitMessage });
940
+ Underpost.repo.manageBackupRepo({ repoName, operation: 'push' });
941
+ processedRepos.add(`${repoName}-committed`);
1118
942
  }
1119
943
  }
1120
944
 
1121
- // Commit and push to Git if enabled - execute only once per repository
1122
- if (options.export === true && options.git === true && !processedRepos.has(`${repoName}-committed`)) {
1123
- const commitMessage = `${new Date(newBackupTimestamp).toLocaleDateString()} ${new Date(
1124
- newBackupTimestamp,
1125
- ).toLocaleTimeString()}`;
1126
- Underpost.db._manageGitRepo({ repoName, operation: 'commit', message: commitMessage });
1127
- Underpost.db._manageGitRepo({ repoName, operation: 'push' });
1128
- processedRepos.add(`${repoName}-committed`);
1129
- }
945
+ logger.info('Database operation completed successfully');
946
+ } catch (error) {
947
+ logger.error('Database operation failed', { error: error.message });
948
+ throw error;
1130
949
  }
1131
-
1132
- logger.info('Database operation completed successfully');
1133
950
  },
1134
951
 
1135
952
  /**
@@ -1141,6 +958,8 @@ class UnderpostDB {
1141
958
  * @param {string} [deployId=process.env.DEFAULT_DEPLOY_ID] - The deployment ID.
1142
959
  * @param {string} [host=process.env.DEFAULT_DEPLOY_HOST] - The host identifier.
1143
960
  * @param {string} [path=process.env.DEFAULT_DEPLOY_PATH] - The path identifier.
961
+ * @param {object} [options] - Options.
962
+ * @param {boolean} [options.dev=false] - Development mode flag.
1144
963
  * @return {Promise<void>} Resolves when metadata creation is complete.
1145
964
  * @throws {Error} If database configuration is invalid or connection fails.
1146
965
  */
@@ -1148,160 +967,181 @@ class UnderpostDB {
1148
967
  deployId = process.env.DEFAULT_DEPLOY_ID,
1149
968
  host = process.env.DEFAULT_DEPLOY_HOST,
1150
969
  path = process.env.DEFAULT_DEPLOY_PATH,
970
+ options = { dev: false },
1151
971
  ) {
1152
- deployId = deployId ? deployId : process.env.DEFAULT_DEPLOY_ID;
1153
- host = host ? host : process.env.DEFAULT_DEPLOY_HOST;
1154
- path = path ? path : process.env.DEFAULT_DEPLOY_PATH;
972
+ try {
973
+ loadCronDeployEnv();
974
+ deployId = deployId ? deployId : process.env.DEFAULT_DEPLOY_ID;
975
+ host = host ? host : process.env.DEFAULT_DEPLOY_HOST;
976
+ path = path ? path : process.env.DEFAULT_DEPLOY_PATH;
1155
977
 
1156
- logger.info('Creating cluster metadata', { deployId, host, path });
978
+ logger.info('Creating cluster metadata', { deployId, host, path });
1157
979
 
1158
- const env = 'production';
1159
- const deployListPath = './engine-private/deploy/dd.router';
980
+ const env = 'production';
981
+ const deployListPath = './engine-private/deploy/dd.router';
1160
982
 
1161
- if (!fs.existsSync(deployListPath)) {
1162
- logger.error('Deploy router file not found', { path: deployListPath });
1163
- throw new Error(`Deploy router file not found: ${deployListPath}`);
1164
- }
983
+ if (!fs.existsSync(deployListPath)) {
984
+ logger.error('Deploy router file not found', { path: deployListPath });
985
+ throw new Error(`Deploy router file not found: ${deployListPath}`);
986
+ }
1165
987
 
1166
- const deployList = fs.readFileSync(deployListPath, 'utf8').split(',');
988
+ const deployList = fs.readFileSync(deployListPath, 'utf8').split(',');
1167
989
 
1168
- const confServerPath = `./engine-private/conf/${deployId}/conf.server.json`;
1169
- if (!fs.existsSync(confServerPath)) {
1170
- logger.error('Server configuration not found', { path: confServerPath });
1171
- throw new Error(`Server configuration not found: ${confServerPath}`);
1172
- }
990
+ const confServerPath = `./engine-private/conf/${deployId}/conf.server.json`;
991
+ if (!fs.existsSync(confServerPath)) {
992
+ logger.error('Server configuration not found', { path: confServerPath });
993
+ throw new Error(`Server configuration not found: ${confServerPath}`);
994
+ }
1173
995
 
1174
- const { db } = JSON.parse(fs.readFileSync(confServerPath, 'utf8'))[host][path];
996
+ const { db } = loadConfServerJson(confServerPath, { resolve: true })[host][path];
1175
997
 
1176
- try {
1177
- await DataBaseProvider.load({ apis: ['instance', 'cron'], host, path, db });
998
+ const maxRetries = 5;
999
+ const retryDelay = 3000;
1000
+ for (let attempt = 1; attempt <= maxRetries; attempt++) {
1001
+ try {
1002
+ await DataBaseProvider.load({ apis: ['instance', 'cron'], host, path, db });
1003
+ break;
1004
+ } catch (err) {
1005
+ if (attempt === maxRetries) {
1006
+ logger.error('Failed to connect to database after retries', { attempts: maxRetries, error: err.message });
1007
+ throw err;
1008
+ }
1009
+ logger.warn('Database connection failed, retrying...', { attempt, maxRetries, error: err.message });
1010
+ await timer(retryDelay);
1011
+ }
1012
+ }
1178
1013
 
1179
- /** @type {import('../api/instance/instance.model.js').InstanceModel} */
1180
- const Instance = DataBaseProvider.instance[`${host}${path}`].mongoose.models.Instance;
1014
+ try {
1015
+ /** @type {import('../api/instance/instance.model.js').InstanceModel} */
1016
+ const Instance = DataBaseProvider.instance[`${host}${path}`].mongoose.models.Instance;
1181
1017
 
1182
- await Instance.deleteMany();
1183
- logger.info('Cleared existing instance metadata');
1018
+ await Instance.deleteMany();
1019
+ logger.info('Cleared existing instance metadata');
1184
1020
 
1185
- for (const _deployId of deployList) {
1186
- const deployId = _deployId.trim();
1187
- if (!deployId) continue;
1021
+ for (const _deployId of deployList) {
1022
+ const deployId = _deployId.trim();
1023
+ if (!deployId) continue;
1188
1024
 
1189
- logger.info('Processing deployment for metadata', { deployId });
1025
+ logger.info('Processing deployment for metadata', { deployId });
1190
1026
 
1191
- const confServerPath = `./engine-private/conf/${deployId}/conf.server.json`;
1192
- if (!fs.existsSync(confServerPath)) {
1193
- logger.warn('Configuration not found for deployment', { deployId, path: confServerPath });
1194
- continue;
1195
- }
1027
+ const confServerPath = `./engine-private/conf/${deployId}/conf.server.json`;
1028
+ if (!fs.existsSync(confServerPath)) {
1029
+ logger.warn('Configuration not found for deployment', { deployId, path: confServerPath });
1030
+ continue;
1031
+ }
1196
1032
 
1197
- const confServer = loadReplicas(deployId, JSON.parse(fs.readFileSync(confServerPath, 'utf8')));
1198
- const router = await Underpost.deploy.routerFactory(deployId, env);
1199
- const pathPortAssignmentData = await pathPortAssignmentFactory(deployId, router, confServer);
1033
+ const confServer = loadReplicas(deployId, loadConfServerJson(confServerPath, { resolve: true }));
1034
+ const router = await Underpost.deploy.routerFactory(deployId, env);
1035
+ const pathPortAssignmentData = await pathPortAssignmentFactory(deployId, router, confServer);
1200
1036
 
1201
- for (const host of Object.keys(confServer)) {
1202
- for (const { path, port } of pathPortAssignmentData[host]) {
1203
- if (!confServer[host][path]) continue;
1037
+ for (const host of Object.keys(confServer)) {
1038
+ for (const { path, port } of pathPortAssignmentData[host]) {
1039
+ if (!confServer[host][path]) continue;
1204
1040
 
1205
- const { client, runtime, apis, peer } = confServer[host][path];
1041
+ const { client, runtime, apis, peer } = confServer[host][path];
1206
1042
 
1207
- // Save main instance
1208
- {
1209
- const body = {
1210
- deployId,
1211
- host,
1212
- path,
1213
- port,
1214
- client,
1215
- runtime,
1216
- apis,
1217
- };
1043
+ // Save main instance
1044
+ {
1045
+ const body = {
1046
+ deployId,
1047
+ host,
1048
+ path,
1049
+ port,
1050
+ client,
1051
+ runtime,
1052
+ apis,
1053
+ };
1054
+
1055
+ logger.info('Saving instance metadata', body);
1056
+ await new Instance(body).save();
1057
+ }
1218
1058
 
1219
- logger.info('Saving instance metadata', body);
1220
- await new Instance(body).save();
1059
+ // Save peer instance if exists
1060
+ if (peer) {
1061
+ const body = {
1062
+ deployId,
1063
+ host,
1064
+ path: path === '/' ? '/peer' : `${path}/peer`,
1065
+ port: port + 1,
1066
+ runtime: 'nodejs',
1067
+ };
1068
+
1069
+ logger.info('Saving peer instance metadata', body);
1070
+ await new Instance(body).save();
1071
+ }
1221
1072
  }
1073
+ }
1222
1074
 
1223
- // Save peer instance if exists
1224
- if (peer) {
1075
+ // Process additional instances
1076
+ const confInstancesPath = `./engine-private/conf/${deployId}/conf.instances.json`;
1077
+ if (fs.existsSync(confInstancesPath)) {
1078
+ const confInstances = JSON.parse(fs.readFileSync(confInstancesPath, 'utf8'));
1079
+ for (const instance of confInstances) {
1080
+ const { id, host, path, fromPort, metadata } = instance;
1081
+ const { runtime } = metadata;
1225
1082
  const body = {
1226
1083
  deployId,
1227
1084
  host,
1228
- path: path === '/' ? '/peer' : `${path}/peer`,
1229
- port: port + 1,
1230
- runtime: 'nodejs',
1085
+ path,
1086
+ port: fromPort,
1087
+ client: id,
1088
+ runtime,
1231
1089
  };
1232
-
1233
- logger.info('Saving peer instance metadata', body);
1090
+ logger.info('Saving additional instance metadata', body);
1234
1091
  await new Instance(body).save();
1235
1092
  }
1236
1093
  }
1237
1094
  }
1238
-
1239
- // Process additional instances
1240
- const confInstancesPath = `./engine-private/conf/${deployId}/conf.instances.json`;
1241
- if (fs.existsSync(confInstancesPath)) {
1242
- const confInstances = JSON.parse(fs.readFileSync(confInstancesPath, 'utf8'));
1243
- for (const instance of confInstances) {
1244
- const { id, host, path, fromPort, metadata } = instance;
1245
- const { runtime } = metadata;
1246
- const body = {
1247
- deployId,
1248
- host,
1249
- path,
1250
- port: fromPort,
1251
- client: id,
1252
- runtime,
1253
- };
1254
- logger.info('Saving additional instance metadata', body);
1255
- await new Instance(body).save();
1256
- }
1257
- }
1095
+ } catch (error) {
1096
+ logger.error('Failed to create instance metadata', { error: error.message });
1097
+ throw error;
1258
1098
  }
1259
- } catch (error) {
1260
- logger.error('Failed to create instance metadata', { error: error.message, stack: error.stack });
1261
- throw error;
1262
- }
1263
1099
 
1264
- try {
1265
- const cronDeployPath = './engine-private/deploy/dd.cron';
1266
- if (!fs.existsSync(cronDeployPath)) {
1267
- logger.warn('Cron deploy file not found', { path: cronDeployPath });
1268
- return;
1269
- }
1100
+ try {
1101
+ const cronDeployPath = './engine-private/deploy/dd.cron';
1102
+ if (!fs.existsSync(cronDeployPath)) {
1103
+ logger.warn('Cron deploy file not found', { path: cronDeployPath });
1104
+ return;
1105
+ }
1270
1106
 
1271
- const cronDeployId = fs.readFileSync(cronDeployPath, 'utf8').trim();
1272
- const confCronPath = `./engine-private/conf/${cronDeployId}/conf.cron.json`;
1107
+ const cronDeployId = fs.readFileSync(cronDeployPath, 'utf8').trim();
1108
+ const confCronPath = `./engine-private/conf/${cronDeployId}/conf.cron.json`;
1273
1109
 
1274
- if (!fs.existsSync(confCronPath)) {
1275
- logger.warn('Cron configuration not found', { path: confCronPath });
1276
- return;
1277
- }
1110
+ if (!fs.existsSync(confCronPath)) {
1111
+ logger.warn('Cron configuration not found', { path: confCronPath });
1112
+ return;
1113
+ }
1278
1114
 
1279
- const confCron = JSON.parse(fs.readFileSync(confCronPath, 'utf8'));
1115
+ const confCron = JSON.parse(fs.readFileSync(confCronPath, 'utf8'));
1280
1116
 
1281
- await DataBaseProvider.load({ apis: ['cron'], host, path, db });
1117
+ await DataBaseProvider.load({ apis: ['cron'], host, path, db });
1282
1118
 
1283
- /** @type {import('../api/cron/cron.model.js').CronModel} */
1284
- const Cron = DataBaseProvider.instance[`${host}${path}`].mongoose.models.Cron;
1119
+ /** @type {import('../api/cron/cron.model.js').CronModel} */
1120
+ const Cron = DataBaseProvider.instance[`${host}${path}`].mongoose.models.Cron;
1285
1121
 
1286
- await Cron.deleteMany();
1287
- logger.info('Cleared existing cron metadata');
1122
+ await Cron.deleteMany();
1123
+ logger.info('Cleared existing cron metadata');
1288
1124
 
1289
- for (const jobId of Object.keys(confCron.jobs)) {
1290
- const body = {
1291
- jobId,
1292
- deployId: Underpost.cron.getRelatedDeployIdList(jobId),
1293
- expression: confCron.jobs[jobId].expression,
1294
- enabled: confCron.jobs[jobId].enabled,
1295
- };
1296
- logger.info('Saving cron metadata', body);
1297
- await new Cron(body).save();
1125
+ for (const jobId of Object.keys(confCron.jobs)) {
1126
+ const body = {
1127
+ jobId,
1128
+ deployId: Underpost.cron.getRelatedDeployIdList(jobId),
1129
+ expression: confCron.jobs[jobId].expression,
1130
+ enabled: confCron.jobs[jobId].enabled,
1131
+ };
1132
+ logger.info('Saving cron metadata', body);
1133
+ await new Cron(body).save();
1134
+ }
1135
+ } catch (error) {
1136
+ logger.error('Failed to create cron metadata', { error: error.message });
1298
1137
  }
1138
+
1139
+ await DataBaseProvider.instance[`${host}${path}`].mongoose.close();
1140
+ logger.info('Cluster metadata creation completed');
1299
1141
  } catch (error) {
1300
- logger.error('Failed to create cron metadata', { error: error.message, stack: error.stack });
1142
+ logger.error('Cluster metadata creation failed', { error: error.message });
1143
+ throw error;
1301
1144
  }
1302
-
1303
- await DataBaseProvider.instance[`${host}${path}`].mongoose.close();
1304
- logger.info('Cluster metadata creation completed');
1305
1145
  },
1306
1146
 
1307
1147
  /**
@@ -1315,6 +1155,7 @@ class UnderpostDB {
1315
1155
  * @param {string} [options.hosts=''] - Comma-separated list of hosts to filter.
1316
1156
  * @param {string} [options.paths=''] - Comma-separated list of paths to filter.
1317
1157
  * @param {boolean} [options.dryRun=false] - If true, only reports what would be deleted.
1158
+ * @param {boolean} [options.dev=false] - Development mode flag.
1318
1159
  * @return {Promise<void>} Resolves when clean operation is complete.
1319
1160
  */
1320
1161
  async cleanFsCollection(
@@ -1323,203 +1164,220 @@ class UnderpostDB {
1323
1164
  hosts: '',
1324
1165
  paths: '',
1325
1166
  dryRun: false,
1167
+ dev: false,
1326
1168
  },
1327
1169
  ) {
1328
- if (deployList === 'dd') deployList = fs.readFileSync(`./engine-private/deploy/dd.router`, 'utf8');
1329
-
1330
- logger.info('Starting File collection cleanup', { deployList, options });
1331
-
1332
- // Load file.ref.json to know which models reference File
1333
- const fileRefPath = './src/api/file/file.ref.json';
1334
- if (!fs.existsSync(fileRefPath)) {
1335
- logger.error('file.ref.json not found', { path: fileRefPath });
1336
- return;
1337
- }
1170
+ const firstDeployId = deployList !== 'dd' ? deployList.split(',')[0].trim() : '';
1171
+ try {
1172
+ loadCronDeployEnv();
1173
+ if (deployList === 'dd') deployList = fs.readFileSync(`./engine-private/deploy/dd.router`, 'utf8');
1338
1174
 
1339
- const fileRefData = JSON.parse(fs.readFileSync(fileRefPath, 'utf8'));
1340
- logger.info('Loaded file reference configuration', { apis: fileRefData.length });
1175
+ logger.info('Starting File collection cleanup', { deployList, options });
1341
1176
 
1342
- // Filter hosts and paths if specified
1343
- const filterHosts = options.hosts ? options.hosts.split(',').map((h) => h.trim()) : [];
1344
- const filterPaths = options.paths ? options.paths.split(',').map((p) => p.trim()) : [];
1177
+ // Load file.ref.json to know which models reference File
1178
+ const fileRefPath = './src/api/file/file.ref.json';
1179
+ if (!fs.existsSync(fileRefPath)) {
1180
+ logger.error('file.ref.json not found', { path: fileRefPath });
1181
+ return;
1182
+ }
1345
1183
 
1346
- // Track all connections to close them at the end
1347
- const connectionsToClose = [];
1184
+ const fileRefData = JSON.parse(fs.readFileSync(fileRefPath, 'utf8'));
1185
+ logger.info('Loaded file reference configuration', { apis: fileRefData.length });
1348
1186
 
1349
- for (const _deployId of deployList.split(',')) {
1350
- const deployId = _deployId.trim();
1351
- if (!deployId) continue;
1187
+ // Filter hosts and paths if specified
1188
+ const filterHosts = options.hosts ? options.hosts.split(',').map((h) => h.trim()) : [];
1189
+ const filterPaths = options.paths ? options.paths.split(',').map((p) => p.trim()) : [];
1352
1190
 
1353
- logger.info('Processing deployment for File cleanup', { deployId });
1191
+ // Track all connections to close them at the end
1192
+ const connectionsToClose = [];
1354
1193
 
1355
- // Load server configuration
1356
- const confServerPath = `./engine-private/conf/${deployId}/conf.server.json`;
1357
- if (!fs.existsSync(confServerPath)) {
1358
- logger.error('Configuration file not found', { path: confServerPath });
1359
- continue;
1360
- }
1194
+ for (const _deployId of deployList.split(',')) {
1195
+ const deployId = _deployId.trim();
1196
+ if (!deployId) continue;
1361
1197
 
1362
- const confServer = JSON.parse(fs.readFileSync(confServerPath, 'utf8'));
1198
+ logger.info('Processing deployment for File cleanup', { deployId });
1363
1199
 
1364
- // Process each host+path combination
1365
- for (const host of Object.keys(confServer)) {
1366
- if (filterHosts.length > 0 && !filterHosts.includes(host)) continue;
1200
+ // Load server configuration
1201
+ const confServerPath = `./engine-private/conf/${deployId}/conf.server.json`;
1202
+ if (!fs.existsSync(confServerPath)) {
1203
+ logger.error('Configuration file not found', { path: confServerPath });
1204
+ continue;
1205
+ }
1367
1206
 
1368
- for (const path of Object.keys(confServer[host])) {
1369
- if (filterPaths.length > 0 && !filterPaths.includes(path)) continue;
1207
+ const confServer = loadConfServerJson(confServerPath, { resolve: true });
1370
1208
 
1371
- const { db, apis } = confServer[host][path];
1372
- if (!db || !apis) continue;
1209
+ // Process each host+path combination
1210
+ for (const host of Object.keys(confServer)) {
1211
+ if (filterHosts.length > 0 && !filterHosts.includes(host)) continue;
1373
1212
 
1374
- // Check if 'file' api is in the apis list
1375
- if (!apis.includes('file')) {
1376
- logger.info('Skipping - no file api in configuration', { host, path });
1377
- continue;
1378
- }
1213
+ for (const path of Object.keys(confServer[host])) {
1214
+ if (filterPaths.length > 0 && !filterPaths.includes(path)) continue;
1379
1215
 
1380
- // logger.info('Processing host+path with file api', { host, path, db: db.name });
1216
+ const { db, apis } = confServer[host][path];
1217
+ if (!db || !apis) continue;
1381
1218
 
1382
- try {
1383
- // Connect to database
1384
- const dbProvider = await DataBaseProvider.load({ apis, host, path, db });
1385
- if (!dbProvider || !dbProvider.models) {
1386
- logger.error('Failed to load database provider', { host, path });
1219
+ // Check if 'file' api is in the apis list
1220
+ if (!apis.includes('file')) {
1221
+ logger.info('Skipping - no file api in configuration', { host, path });
1387
1222
  continue;
1388
1223
  }
1389
1224
 
1390
- const { models } = dbProvider;
1225
+ // logger.info('Processing host+path with file api', { host, path, db: db.name });
1226
+
1227
+ try {
1228
+ // Connect to database with retry
1229
+ let dbProvider;
1230
+ for (let attempt = 1; attempt <= 3; attempt++) {
1231
+ try {
1232
+ dbProvider = await DataBaseProvider.load({ apis, host, path, db });
1233
+ break;
1234
+ } catch (err) {
1235
+ if (attempt === 3) throw err;
1236
+ logger.warn('Database connection failed, retrying...', { attempt, host, path, error: err.message });
1237
+ await timer(3000);
1238
+ }
1239
+ }
1240
+ if (!dbProvider || !dbProvider.models) {
1241
+ logger.error('Failed to load database provider', { host, path });
1242
+ continue;
1243
+ }
1391
1244
 
1392
- // Track this connection for cleanup
1393
- connectionsToClose.push({ host, path, dbProvider });
1245
+ const { models } = dbProvider;
1394
1246
 
1395
- // Check if File model exists
1396
- if (!models.File) {
1397
- logger.warn('File model not loaded', { host, path });
1398
- continue;
1399
- }
1247
+ // Track this connection for cleanup
1248
+ connectionsToClose.push({ host, path, dbProvider });
1400
1249
 
1401
- // Get all File documents
1402
- const allFiles = await models.File.find({}, '_id').lean();
1403
- logger.info('Found File documents', { count: allFiles.length, host, path });
1250
+ // Check if File model exists
1251
+ if (!models.File) {
1252
+ logger.warn('File model not loaded', { host, path });
1253
+ continue;
1254
+ }
1404
1255
 
1405
- if (allFiles.length === 0) continue;
1256
+ // Get all File documents
1257
+ const allFiles = await models.File.find({}, '_id').lean();
1258
+ logger.info('Found File documents', { count: allFiles.length, host, path });
1406
1259
 
1407
- // Track which File IDs are referenced
1408
- const referencedFileIds = new Set();
1260
+ if (allFiles.length === 0) continue;
1409
1261
 
1410
- // Check each API from file.ref.json
1411
- for (const refConfig of fileRefData) {
1412
- const { api, model: modelFields } = refConfig;
1262
+ // Track which File IDs are referenced
1263
+ const referencedFileIds = new Set();
1413
1264
 
1414
- // Check if this API is loaded in current context
1415
- const modelName = api
1416
- .split('-')
1417
- .map((w) => w.charAt(0).toUpperCase() + w.slice(1))
1418
- .join('');
1419
- const Model = models[modelName];
1265
+ // Check each API from file.ref.json
1266
+ for (const refConfig of fileRefData) {
1267
+ const { api, model: modelFields } = refConfig;
1420
1268
 
1421
- if (!Model) {
1422
- logger.debug('Model not loaded in current context', { api, modelName, host, path });
1423
- continue;
1424
- }
1269
+ // Check if this API is loaded in current context
1270
+ const modelName = api
1271
+ .split('-')
1272
+ .map((w) => w.charAt(0).toUpperCase() + w.slice(1))
1273
+ .join('');
1274
+ const Model = models[modelName];
1425
1275
 
1426
- logger.info('Checking references in model', { api, modelName });
1276
+ if (!Model) {
1277
+ logger.debug('Model not loaded in current context', { api, modelName, host, path });
1278
+ continue;
1279
+ }
1427
1280
 
1428
- // Helper function to recursively check field references
1429
- const checkFieldReferences = async (fieldPath, fieldConfig) => {
1430
- for (const [fieldName, fieldValue] of Object.entries(fieldConfig)) {
1431
- const currentPath = fieldPath ? `${fieldPath}.${fieldName}` : fieldName;
1281
+ logger.info('Checking references in model', { api, modelName });
1432
1282
 
1433
- if (fieldValue === true) {
1434
- // This is a File reference field
1435
- const query = {};
1436
- query[currentPath] = { $exists: true, $ne: null };
1283
+ // Helper function to recursively check field references
1284
+ const checkFieldReferences = async (fieldPath, fieldConfig) => {
1285
+ for (const [fieldName, fieldValue] of Object.entries(fieldConfig)) {
1286
+ const currentPath = fieldPath ? `${fieldPath}.${fieldName}` : fieldName;
1437
1287
 
1438
- const docs = await Model.find(query, currentPath).lean();
1288
+ if (fieldValue === true) {
1289
+ // This is a File reference field
1290
+ const query = {};
1291
+ query[currentPath] = { $exists: true, $ne: null };
1439
1292
 
1440
- for (const doc of docs) {
1441
- // Navigate to the nested field
1442
- const parts = currentPath.split('.');
1443
- let value = doc;
1444
- for (const part of parts) {
1445
- value = value?.[part];
1446
- }
1293
+ const docs = await Model.find(query, currentPath).lean();
1447
1294
 
1448
- if (value) {
1449
- if (Array.isArray(value)) {
1450
- value.forEach((id) => id && referencedFileIds.add(id.toString()));
1451
- } else {
1452
- referencedFileIds.add(value.toString());
1295
+ for (const doc of docs) {
1296
+ // Navigate to the nested field
1297
+ const parts = currentPath.split('.');
1298
+ let value = doc;
1299
+ for (const part of parts) {
1300
+ value = value?.[part];
1301
+ }
1302
+
1303
+ if (value) {
1304
+ if (Array.isArray(value)) {
1305
+ value.forEach((id) => id && referencedFileIds.add(id.toString()));
1306
+ } else {
1307
+ referencedFileIds.add(value.toString());
1308
+ }
1453
1309
  }
1454
1310
  }
1455
- }
1456
1311
 
1457
- logger.info('Found references', {
1458
- model: modelName,
1459
- field: currentPath,
1460
- count: docs.length,
1461
- });
1462
- } else if (typeof fieldValue === 'object') {
1463
- // Nested object, recurse
1464
- await checkFieldReferences(currentPath, fieldValue);
1312
+ logger.info('Found references', {
1313
+ model: modelName,
1314
+ field: currentPath,
1315
+ count: docs.length,
1316
+ });
1317
+ } else if (typeof fieldValue === 'object') {
1318
+ // Nested object, recurse
1319
+ await checkFieldReferences(currentPath, fieldValue);
1320
+ }
1465
1321
  }
1466
- }
1467
- };
1468
-
1469
- await checkFieldReferences('', modelFields);
1470
- }
1322
+ };
1471
1323
 
1472
- logger.info('Total referenced File IDs', { count: referencedFileIds.size, host, path });
1324
+ await checkFieldReferences('', modelFields);
1325
+ }
1473
1326
 
1474
- // Find orphaned files
1475
- const orphanedFiles = allFiles.filter((file) => !referencedFileIds.has(file._id.toString()));
1327
+ logger.info('Total referenced File IDs', { count: referencedFileIds.size, host, path });
1476
1328
 
1477
- if (orphanedFiles.length === 0) {
1478
- logger.info('No orphaned files found', { host, path });
1479
- } else {
1480
- logger.info('Found orphaned files', { count: orphanedFiles.length, host, path });
1329
+ // Find orphaned files
1330
+ const orphanedFiles = allFiles.filter((file) => !referencedFileIds.has(file._id.toString()));
1481
1331
 
1482
- if (options.dryRun) {
1483
- logger.info('Dry run - would delete files', {
1484
- count: orphanedFiles.length,
1485
- ids: orphanedFiles.map((f) => f._id.toString()),
1486
- });
1332
+ if (orphanedFiles.length === 0) {
1333
+ logger.info('No orphaned files found', { host, path });
1487
1334
  } else {
1488
- const orphanedIds = orphanedFiles.map((f) => f._id);
1489
- const deleteResult = await models.File.deleteMany({ _id: { $in: orphanedIds } });
1490
- logger.info('Deleted orphaned files', {
1491
- deletedCount: deleteResult.deletedCount,
1492
- host,
1493
- path,
1494
- });
1335
+ logger.info('Found orphaned files', { count: orphanedFiles.length, host, path });
1336
+
1337
+ if (options.dryRun) {
1338
+ logger.info('Dry run - would delete files', {
1339
+ count: orphanedFiles.length,
1340
+ ids: orphanedFiles.map((f) => f._id.toString()),
1341
+ });
1342
+ } else {
1343
+ const orphanedIds = orphanedFiles.map((f) => f._id);
1344
+ const deleteResult = await models.File.deleteMany({ _id: { $in: orphanedIds } });
1345
+ logger.info('Deleted orphaned files', {
1346
+ deletedCount: deleteResult.deletedCount,
1347
+ host,
1348
+ path,
1349
+ });
1350
+ }
1495
1351
  }
1352
+ } catch (error) {
1353
+ logger.error('Error processing host+path', {
1354
+ host,
1355
+ path,
1356
+ error: error.message,
1357
+ });
1496
1358
  }
1497
- } catch (error) {
1498
- logger.error('Error processing host+path', {
1499
- host,
1500
- path,
1501
- error: error.message,
1502
- stack: error.stack,
1503
- });
1504
1359
  }
1505
1360
  }
1506
1361
  }
1507
- }
1508
1362
 
1509
- // Close all connections
1510
- logger.info('Closing all database connections', { count: connectionsToClose.length });
1511
- for (const { host, path, dbProvider } of connectionsToClose) {
1512
- try {
1513
- if (dbProvider && dbProvider.close) {
1514
- await dbProvider.close();
1515
- logger.info('Connection closed', { host, path });
1363
+ // Close all connections
1364
+ logger.info('Closing all database connections', { count: connectionsToClose.length });
1365
+ for (const { host, path, dbProvider } of connectionsToClose) {
1366
+ try {
1367
+ if (dbProvider && dbProvider.close) {
1368
+ await dbProvider.close();
1369
+ logger.info('Connection closed', { host, path });
1370
+ }
1371
+ } catch (error) {
1372
+ logger.error('Error closing connection', { host, path, error: error.message });
1516
1373
  }
1517
- } catch (error) {
1518
- logger.error('Error closing connection', { host, path, error: error.message });
1519
1374
  }
1520
- }
1521
1375
 
1522
- logger.info('File collection cleanup completed');
1376
+ logger.info('File collection cleanup completed');
1377
+ } catch (error) {
1378
+ logger.error('File collection cleanup failed', { error: error.message });
1379
+ throw error;
1380
+ }
1523
1381
  },
1524
1382
 
1525
1383
  /**
@@ -1538,6 +1396,7 @@ class UnderpostDB {
1538
1396
  * @param {boolean} [options.export=false] - Export metadata to backup.
1539
1397
  * @param {boolean} [options.instances=false] - Process instances collection.
1540
1398
  * @param {boolean} [options.crons=false] - Process crons collection.
1399
+ * @param {boolean} [options.dev=false] - Development mode flag.
1541
1400
  * @return {Promise<void>} Resolves when backup operation is complete.
1542
1401
  */
1543
1402
  async clusterMetadataBackupCallback(
@@ -1551,69 +1410,76 @@ class UnderpostDB {
1551
1410
  export: false,
1552
1411
  instances: false,
1553
1412
  crons: false,
1413
+ dev: false,
1554
1414
  },
1555
1415
  ) {
1556
- deployId = deployId ? deployId : process.env.DEFAULT_DEPLOY_ID;
1557
- host = host ? host : process.env.DEFAULT_DEPLOY_HOST;
1558
- path = path ? path : process.env.DEFAULT_DEPLOY_PATH;
1559
-
1560
- logger.info('Starting cluster metadata backup operation', {
1561
- deployId,
1562
- host,
1563
- path,
1564
- options,
1565
- });
1566
-
1567
- if (options.generate === true) {
1568
- logger.info('Generating cluster metadata');
1569
- await Underpost.db.clusterMetadataFactory(deployId, host, path);
1570
- }
1416
+ try {
1417
+ loadCronDeployEnv();
1418
+ deployId = deployId ? deployId : process.env.DEFAULT_DEPLOY_ID;
1419
+ host = host ? host : process.env.DEFAULT_DEPLOY_HOST;
1420
+ path = path ? path : process.env.DEFAULT_DEPLOY_PATH;
1421
+
1422
+ logger.info('Starting cluster metadata backup operation', {
1423
+ deployId,
1424
+ host,
1425
+ path,
1426
+ options,
1427
+ });
1571
1428
 
1572
- if (options.instances === true) {
1573
- const outputPath = './engine-private/instances';
1574
- if (!fs.existsSync(outputPath)) {
1575
- fs.mkdirSync(outputPath, { recursive: true });
1429
+ if (options.generate === true) {
1430
+ logger.info('Generating cluster metadata');
1431
+ await Underpost.db.clusterMetadataFactory(deployId, host, path);
1576
1432
  }
1577
- const collection = 'instances';
1578
1433
 
1579
- if (options.export === true) {
1580
- logger.info('Exporting instances collection', { outputPath });
1581
- shellExec(
1582
- `node bin db --export --primary-pod --collections ${collection} --out-path ${outputPath} --hosts ${host} --paths '${path}' ${deployId}`,
1583
- );
1584
- }
1434
+ if (options.instances === true) {
1435
+ const outputPath = './engine-private/instances';
1436
+ if (!fs.existsSync(outputPath)) {
1437
+ fs.mkdirSync(outputPath, { recursive: true });
1438
+ }
1439
+ const collection = 'instances';
1585
1440
 
1586
- if (options.import === true) {
1587
- logger.info('Importing instances collection', { outputPath });
1588
- shellExec(
1589
- `node bin db --import --primary-pod --drop --preserveUUID --out-path ${outputPath} --hosts ${host} --paths '${path}' ${deployId}`,
1590
- );
1591
- }
1592
- }
1441
+ if (options.export === true) {
1442
+ logger.info('Exporting instances collection', { outputPath });
1443
+ shellExec(
1444
+ `node bin db --export --primary-pod --collections ${collection} --out-path ${outputPath} --hosts ${host} --paths '${path}' ${deployId}`,
1445
+ );
1446
+ }
1593
1447
 
1594
- if (options.crons === true) {
1595
- const outputPath = './engine-private/crons';
1596
- if (!fs.existsSync(outputPath)) {
1597
- fs.mkdirSync(outputPath, { recursive: true });
1448
+ if (options.import === true) {
1449
+ logger.info('Importing instances collection', { outputPath });
1450
+ shellExec(
1451
+ `node bin db --import --primary-pod --drop --preserveUUID --out-path ${outputPath} --hosts ${host} --paths '${path}' ${deployId}`,
1452
+ );
1453
+ }
1598
1454
  }
1599
- const collection = 'crons';
1600
1455
 
1601
- if (options.export === true) {
1602
- logger.info('Exporting crons collection', { outputPath });
1603
- shellExec(
1604
- `node bin db --export --primary-pod --collections ${collection} --out-path ${outputPath} --hosts ${host} --paths '${path}' ${deployId}`,
1605
- );
1606
- }
1456
+ if (options.crons === true) {
1457
+ const outputPath = './engine-private/crons';
1458
+ if (!fs.existsSync(outputPath)) {
1459
+ fs.mkdirSync(outputPath, { recursive: true });
1460
+ }
1461
+ const collection = 'crons';
1462
+
1463
+ if (options.export === true) {
1464
+ logger.info('Exporting crons collection', { outputPath });
1465
+ shellExec(
1466
+ `node bin db --export --primary-pod --collections ${collection} --out-path ${outputPath} --hosts ${host} --paths '${path}' ${deployId}`,
1467
+ );
1468
+ }
1607
1469
 
1608
- if (options.import === true) {
1609
- logger.info('Importing crons collection', { outputPath });
1610
- shellExec(
1611
- `node bin db --import --primary-pod --drop --preserveUUID --out-path ${outputPath} --hosts ${host} --paths '${path}' ${deployId}`,
1612
- );
1470
+ if (options.import === true) {
1471
+ logger.info('Importing crons collection', { outputPath });
1472
+ shellExec(
1473
+ `node bin db --import --primary-pod --drop --preserveUUID --out-path ${outputPath} --hosts ${host} --paths '${path}' ${deployId}`,
1474
+ );
1475
+ }
1613
1476
  }
1614
- }
1615
1477
 
1616
- logger.info('Cluster metadata backup operation completed');
1478
+ logger.info('Cluster metadata backup operation completed');
1479
+ } catch (error) {
1480
+ logger.error('Cluster metadata backup operation failed', { error: error.message });
1481
+ throw error;
1482
+ }
1617
1483
  },
1618
1484
  };
1619
1485
  }