Business Central Backup Automation

Automates the creation of backups of the Business Central database using Azure.

  1. // ==UserScript==
  2. // @name Business Central Backup Automation
  3. // @name:de Business Central Backup-Automatisierung
  4.  
  5. // @description Automates the creation of backups of the Business Central database using Azure.
  6. // @description:de Automatisierung des Erstellens von Backups von Business Central mittels Azure.
  7.  
  8. // @version 1.0.1
  9. // @author Rsge
  10. // @copyright 2025+, Jan G. (Rsge)
  11. // @license All rights reserved
  12. // @icon https://msweugwcas4004-a8arc8v.appservices.weu.businesscentral.dynamics.com/tenant/msweua1602t06326066/tab/92b102bf-7e05-4693-b322-e60777e7602f/Brand/Images/favicon.ico
  13.  
  14. // @namespace https://github.com/Rsge
  15. // @homepageURL https://github.com/Rsge/Business-Central-Auto-Backup
  16. // @supportURL https://github.com/Rsge/Business-Central-Auto-Backup/issues
  17.  
  18. // @match https://portal.azure.com/*
  19. // @match https://businesscentral.dynamics.com/*
  20.  
  21. // @run-at document-idle
  22. // @grant none
  23. // ==/UserScript==
  24.  
  25. (async function() {
  26. 'use strict';
  27.  
  28. // Constants
  29. const T = 1000;
  30. const LOC = window.location;
  31. const LINK_LABEL = "azureSasUrl";
  32. // Resources
  33. const ENV_IDX = 0; // Index (0-based) of environment to backup in list of BC Admin center
  34. const START_AUTOMATION_QUESTION = "Start backup automation?";
  35. const PASTE_SAS_URI_MSG = `<p>Sadly, automatic pasting of the SAS-URL doesn't seem possible.<br>
  36. The SAS-URL will be added to your clipboard.<br>
  37. Please paste it manually using <kbd><kbd>Strg</kbd>+<kbd>V</kbd></kbd>.<br>
  38. After pasting, the export will automatically be started immediately and the tab closed after 5 s.</p>`;
  39.  
  40. // Variables
  41. let i;
  42.  
  43. // Basic functions
  44. function sleep(ms) {
  45. return new Promise(resolve => setTimeout(resolve, ms));
  46. }
  47. function getCookies() {
  48. return document.cookie.split(";")
  49. .map(function(cstr) {
  50. return cstr.trim().split("=");})
  51. .reduce(function(acc, curr) {
  52. acc[curr[0]] = curr[1];
  53. return acc;
  54. }, {});
  55. }
  56. function setCookie(key, value) {
  57. document.cookie = key + "=" + value + "; path=/";
  58. }
  59. function getXthElementByClassName(className, i = 0) {
  60. return document.getElementsByClassName(className)[i];
  61. }
  62. function getElementByClassNameAndTitle(className, title) {
  63. let items = Array.from(document.getElementsByClassName(className));
  64. const foundItem = items.find(
  65. e => e.title.startsWith(title)
  66. );
  67. return foundItem;
  68. }
  69. function getElementByClassNameAndText(className, text) {
  70. let items = Array.from(document.getElementsByClassName(className));
  71. const foundItem = items.find(
  72. e => e.textContent.startsWith(text)
  73. );
  74. return foundItem;
  75. }
  76. async function findClickWait(className, string, t, useText = false) {
  77. let element;
  78. if (useText) {
  79. element = getElementByClassNameAndText(className, string);
  80. } else {
  81. element = getElementByClassNameAndTitle(className, string);
  82. }
  83. if (element) {
  84. element.click();
  85. await sleep(t);
  86. return true;
  87. }
  88. return false;
  89. }
  90.  
  91. /* --------------------------------------------------- */
  92.  
  93. /* Custom dialog boxes */
  94.  
  95. // Base dialog
  96. async function cdialog(id, html) {
  97. // Create dialog HTML.
  98. let dialog = document.createElement("dialog");
  99. dialog.id = id;
  100. dialog.innerHTML = html;
  101. // Add dialog to site and show it.
  102. document.body.appendChild(dialog);
  103. dialog.showModal();
  104. // Wait for click on one of the buttons.
  105. return new Promise(function(resolve) {
  106. let buttons = dialog.getElementsByClassName("button")
  107. for (i = 0; i < buttons.length; i++) {
  108. let result = i == 0;
  109. buttons[i].addEventListener("click", function() {dialog.close(); resolve(result);});
  110. }
  111. });
  112. }
  113.  
  114. // Confirmation dialog
  115. async function yesNoDialog(msg) {
  116. return await cdialog("yesNoDialog", `<p>
  117. <label>${msg}</label>
  118. </p><p class="button-row">
  119. <button name="yesButton" class="button">Ja</button>
  120. <button name="noButton" class="button">Nein</button>
  121. </p>`);
  122. }
  123.  
  124. // Ok dialog
  125. async function okCancelDialog(msg) {
  126. return await cdialog("okCancelDialog", `<p>
  127. <label>${msg}</label>
  128. </p><p class="button-row">
  129. <button name="okButton" class="button">OK</button>
  130. <button name="cancelButton" class="button">Abbrechen</button>
  131. </p>`);
  132. }
  133.  
  134. /* --------------------------------------------------- */
  135.  
  136. /* Main */
  137. // Azure
  138. if (LOC.href.endsWith("azure.com/#home") &&
  139. await yesNoDialog(START_AUTOMATION_QUESTION)) {
  140. // Sidebar
  141. await findClickWait("fxs-topbar-sidebar-collapse-button", "Show portal menu", 0.5*T);
  142. // Storage accounts
  143. await findClickWait("fxs-sidebar-item-link", "Storage accounts", 3*T);
  144. // First storage account
  145. getXthElementByClassName("fxc-gcflink-link").click();
  146. await sleep(3*T);
  147. // Sidebar
  148. await findClickWait("fxs-topbar-sidebar-collapse-button", "Show portal menu", 0.5*T);
  149. // Shared access signature
  150. await findClickWait("fxc-menu-item", "Shared access signature", 4*T, true);
  151. // SAS form
  152. /// Checkboxes
  153. let ucFieldIDs = ["__field__6__", "__field__7__", "__field__8__", // Allowed services
  154. "__field__15__", "__field__16__", "__field__20__", "__field__21__"]; // Allowed permissions
  155. let cFieldIDs = ["__field__10__", "__field__11__", ]; // Allowed resource types
  156. for (let ucFieldID of ucFieldIDs) {
  157. let ucField = document.getElementById(ucFieldID);
  158. if (ucField.ariaChecked == true.toString()) {
  159. ucField.click();
  160. }
  161. }
  162. for (let cFieldID of cFieldIDs) {
  163. let cField = document.getElementById(cFieldID);
  164. if (cField.ariaChecked === false.toString()) {
  165. cField.click();
  166. }
  167. }
  168. /// Date
  169. let endDatePicker = getXthElementByClassName("azc-datePicker", 1);
  170. let datePanelOpener = endDatePicker.children[0].children[1];
  171. datePanelOpener.click();
  172. await sleep(T);
  173. let datePanel = getXthElementByClassName("azc-datePanel");
  174. let todayBox = datePanel.getElementsByClassName("azc-datePanel-selected")[0];
  175. let weekArray = Array.from(todayBox.parentNode.children);
  176. let todayIdx = weekArray.indexOf(todayBox);
  177. let tomorrowBox = weekArray[todayIdx + 1];
  178. tomorrowBox.click();
  179. await sleep(T);
  180. /// Generate
  181. await findClickWait("fxc-simplebutton", "Generate SAS and connection string", 2*T, true);
  182. /// Copy
  183. let encLink = encodeURIComponent(getElementByClassNameAndTitle("azc-input azc-formControl", "https://").value);
  184. // Open BC
  185. let bcWindow = window.open("https://businesscentral.dynamics.com/?noSignUpCheck=1&" + LINK_LABEL + "=" + encLink)
  186. await sleep(T);
  187. // Sidebar
  188. await findClickWait("fxs-topbar-sidebar-collapse-button", "Show portal menu", 0.5*T);
  189. // Containers
  190. await findClickWait("fxc-menu-item", "Containers", 4*T, true);
  191. // First container
  192. getXthElementByClassName("azc-grid-cellContent").click();
  193. await sleep(30*T);
  194. await findClickWait("azc-toolbarButton-label", "Refresh", T, true);
  195. } // BusinessCentral
  196. else if (LOC.host.startsWith("businesscentral")) {
  197. // Normal BC
  198. if (!LOC.pathname.endsWith("/admin")) {
  199. // Get SAS link from URL.
  200. let params = new URLSearchParams(LOC.search);
  201. const Link = encodeURIComponent(params.get(LINK_LABEL));
  202. // If no link found, exit for normal use.
  203. if (!Link) {
  204. return;
  205. }
  206. // Wait for loading of elements.
  207. let ranSettings = false;
  208. let ranAC = false;
  209. let observer = new MutationObserver(function(mutations) {
  210. mutations.forEach(function(mutation) {
  211. // "Disable" observer after Admin Center is clicked.
  212. if (ranAC) {
  213. return;
  214. }
  215. let node = mutation.addedNodes[0];
  216. if (node?.children != null && node.children.length > 0) {
  217. // Open settings dropdown.
  218. const SettingsButtonID = "O365_MainLink_Settings";
  219. if (!ranSettings && node.children[0].id == SettingsButtonID) {
  220. ranSettings = true;
  221. document.getElementById(SettingsButtonID).click();
  222. } // Open Admin Center (in new tab) and close current tab.
  223. // Also set a cookie with the SAS link for use in the Admin Center.
  224. else {
  225. let adminCenter = document.getElementById("AdminCenter")
  226. if (adminCenter) {
  227. ranAC = true;
  228. setCookie(LINK_LABEL, Link)
  229. adminCenter.click();
  230. window.close();
  231. }
  232. }
  233. }
  234. });
  235. });
  236. observer.observe(document.documentElement, {
  237. childList: true,
  238. subtree: true
  239. });
  240. } // BC Admin Center
  241. else {
  242. // Get SAS link from cookie.
  243. const Link = getCookies()[LINK_LABEL];
  244. // If no link found, exit for normal use.
  245. if (!Link || Link.length == 0) {
  246. return;
  247. }
  248. // Environments
  249. findClickWait("ms-Button ms-Button--action ms-Button--command", "Environments", 0);
  250. // Wait for loading of environments.
  251. let run = false;
  252. const EnvListClassName = "ms-List-page"
  253. let observer = new MutationObserver(function(mutations) {
  254. mutations.forEach(async function(mutation) {
  255. let node = mutation.addedNodes[0];
  256. //console.log(node);
  257. if (node?.children != null) {
  258. if (node.className == EnvListClassName) {
  259. // First environment
  260. let envList = getXthElementByClassName(EnvListClassName);
  261. let env = envList.children[ENV_IDX].children[0].children[0].children[0].children[0].children[0];
  262. env.click();
  263. await sleep(T);
  264. // Database dropdown
  265. await findClickWait("ms-Button ms-Button--commandBar ms-Button--hasMenu", "Database", 0.5*T);
  266. // Create export
  267. await findClickWait("ms-ContextualMenu-link", "Create database export");
  268. } else if (node.className.startsWith("ms-Layer ms-Layer--fixed")) {
  269. // Insert link
  270. let sasTxt = getElementByClassNameAndTitle("ms-TextField-field", "SAS URI from Azure");
  271. if (sasTxt) {
  272. let decLink = decodeURIComponent(Link);
  273. await sleep(T);
  274. if (await okCancelDialog(PASTE_SAS_URI_MSG)) {
  275. const inputHandler = async function(e) {
  276. if (e.target.value == decLink) {
  277. await sleep(T);
  278. await findClickWait("ms-Button ms-Button--primary", "Create", 5*T, true);
  279. window.close();
  280. }
  281. }
  282. sasTxt.addEventListener("input", inputHandler);
  283. navigator.clipboard.writeText(decLink);
  284. sasTxt.focus();
  285. }
  286. }
  287. }
  288. }
  289. });
  290. });
  291. observer.observe(document.documentElement, {
  292. childList: true,
  293. subtree: true
  294. });
  295. }
  296. }
  297. })();