Business Central Backup Automation

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

当前为 2025-04-22 提交的版本,查看 最新版本

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Greasemonkey 油猴子Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Userscripts ,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==UserScript==
// @name           Business Central Backup Automation
// @name:de        Business Central Backup-Automatisierung

// @description    Automates the creation of backups of the Business Central database using Azure.
// @description:de Automatisierung des Erstellens von Backups von Business Central mittels Azure.

// @version        1.0.1
// @author         Rsge
// @copyright      2025+, Jan G. (Rsge)
// @license        All rights reserved
// @icon           https://msweugwcas4004-a8arc8v.appservices.weu.businesscentral.dynamics.com/tenant/msweua1602t06326066/tab/92b102bf-7e05-4693-b322-e60777e7602f/Brand/Images/favicon.ico

// @namespace      https://github.com/Rsge
// @homepageURL    https://github.com/Rsge/Business-Central-Auto-Backup
// @supportURL     https://github.com/Rsge/Business-Central-Auto-Backup/issues

// @match          https://portal.azure.com/*
// @match          https://businesscentral.dynamics.com/*

// @run-at         document-idle
// @grant          none
// ==/UserScript==

(async function() {
    'use strict';

  // Constants
  const T = 1000;
  const LOC = window.location;
  const LINK_LABEL = "azureSasUrl";
  // Resources
  const ENV_IDX = 0; // Index (0-based) of environment to backup in list of BC Admin center
  const START_AUTOMATION_QUESTION = "Start backup automation?";
  const PASTE_SAS_URI_MSG = `<p>Sadly, automatic pasting of the SAS-URL doesn't seem possible.<br>
  The SAS-URL will be added to your clipboard.<br>
  Please paste it manually using <kbd><kbd>Strg</kbd>+<kbd>V</kbd></kbd>.<br>
  After pasting, the export will automatically be started immediately and the tab closed after 5 s.</p>`;

  // Variables
  let i;

  // Basic functions
  function sleep(ms) {
    return new Promise(resolve => setTimeout(resolve, ms));
  }
  function getCookies() {
    return document.cookie.split(";")
      .map(function(cstr) {
      return cstr.trim().split("=");})
      .reduce(function(acc, curr) {
      acc[curr[0]] = curr[1];
      return acc;
    }, {});
  }
  function setCookie(key, value) {
    document.cookie = key + "=" + value + "; path=/";
  }
  function getXthElementByClassName(className, i = 0) {
    return document.getElementsByClassName(className)[i];
  }
  function getElementByClassNameAndTitle(className, title) {
    let items = Array.from(document.getElementsByClassName(className));
    const foundItem = items.find(
      e => e.title.startsWith(title)
    );
    return foundItem;
  }
  function getElementByClassNameAndText(className, text) {
    let items = Array.from(document.getElementsByClassName(className));
    const foundItem = items.find(
      e => e.textContent.startsWith(text)
    );
    return foundItem;
  }
  async function findClickWait(className, string, t, useText = false) {
    let element;
    if (useText) {
      element = getElementByClassNameAndText(className, string);
    } else {
      element = getElementByClassNameAndTitle(className, string);
    }
    if (element) {
      element.click();
      await sleep(t);
      return true;
    }
    return false;
  }

  /* --------------------------------------------------- */

  /* Custom dialog boxes */

  // Base dialog
  async function cdialog(id, html) {
    // Create dialog HTML.
    let dialog = document.createElement("dialog");
    dialog.id = id;
    dialog.innerHTML = html;
    // Add dialog to site and show it.
    document.body.appendChild(dialog);
    dialog.showModal();
    // Wait for click on one of the buttons.
    return new Promise(function(resolve) {
      let buttons = dialog.getElementsByClassName("button")
      for (i = 0; i < buttons.length; i++) {
        let result = i == 0;
        buttons[i].addEventListener("click", function() {dialog.close(); resolve(result);});
      }
    });
  }

  // Confirmation dialog
  async function yesNoDialog(msg) {
    return await cdialog("yesNoDialog", `<p>
    <label>${msg}</label>
</p><p class="button-row">
    <button name="yesButton" class="button">Ja</button>
    <button name="noButton" class="button">Nein</button>
</p>`);
  }

  // Ok dialog
  async function okCancelDialog(msg) {
    return await cdialog("okCancelDialog", `<p>
    <label>${msg}</label>
</p><p class="button-row">
    <button name="okButton" class="button">OK</button>
    <button name="cancelButton" class="button">Abbrechen</button>
</p>`);
  }

  /* --------------------------------------------------- */

  /* Main */
  // Azure
  if (LOC.href.endsWith("azure.com/#home") &&
      await yesNoDialog(START_AUTOMATION_QUESTION)) {
    // Sidebar
    await findClickWait("fxs-topbar-sidebar-collapse-button", "Show portal menu", 0.5*T);
    // Storage accounts
    await findClickWait("fxs-sidebar-item-link", "Storage accounts", 3*T);
    // First storage account
    getXthElementByClassName("fxc-gcflink-link").click();
    await sleep(3*T);
    // Sidebar
    await findClickWait("fxs-topbar-sidebar-collapse-button", "Show portal menu", 0.5*T);
    // Shared access signature
    await findClickWait("fxc-menu-item", "Shared access signature", 4*T, true);
    // SAS form
    /// Checkboxes
    let ucFieldIDs = ["__field__6__", "__field__7__", "__field__8__", // Allowed services
                      "__field__15__", "__field__16__", "__field__20__", "__field__21__"]; // Allowed permissions
    let cFieldIDs = ["__field__10__", "__field__11__", ]; // Allowed resource types
    for (let ucFieldID of ucFieldIDs) {
      let ucField = document.getElementById(ucFieldID);
      if (ucField.ariaChecked == true.toString()) {
        ucField.click();
      }
    }
    for (let cFieldID of cFieldIDs) {
      let cField = document.getElementById(cFieldID);
      if (cField.ariaChecked === false.toString()) {
        cField.click();
      }
    }
    /// Date
    let endDatePicker = getXthElementByClassName("azc-datePicker", 1);
    let datePanelOpener = endDatePicker.children[0].children[1];
    datePanelOpener.click();
    await sleep(T);
    let datePanel = getXthElementByClassName("azc-datePanel");
    let todayBox = datePanel.getElementsByClassName("azc-datePanel-selected")[0];
    let weekArray = Array.from(todayBox.parentNode.children);
    let todayIdx = weekArray.indexOf(todayBox);
    let tomorrowBox = weekArray[todayIdx + 1];
    tomorrowBox.click();
    await sleep(T);
    /// Generate
    await findClickWait("fxc-simplebutton", "Generate SAS and connection string", 2*T, true);
    /// Copy
    let encLink = encodeURIComponent(getElementByClassNameAndTitle("azc-input azc-formControl", "https://").value);
    // Open BC
    let bcWindow = window.open("https://businesscentral.dynamics.com/?noSignUpCheck=1&" + LINK_LABEL + "=" + encLink)
    await sleep(T);
    // Sidebar
    await findClickWait("fxs-topbar-sidebar-collapse-button", "Show portal menu", 0.5*T);
    // Containers
    await findClickWait("fxc-menu-item", "Containers", 4*T, true);
    // First container
    getXthElementByClassName("azc-grid-cellContent").click();
    await sleep(30*T);
    await findClickWait("azc-toolbarButton-label", "Refresh", T, true);
  } // BusinessCentral
  else if (LOC.host.startsWith("businesscentral")) {
    // Normal BC
    if (!LOC.pathname.endsWith("/admin")) {
      // Get SAS link from URL.
      let params = new URLSearchParams(LOC.search);
      const Link = encodeURIComponent(params.get(LINK_LABEL));
      // If no link found, exit for normal use.
      if (!Link) {
        return;
      }
      // Wait for loading of elements.
      let ranSettings = false;
      let ranAC = false;
      let observer = new MutationObserver(function(mutations) {
        mutations.forEach(function(mutation) {
          // "Disable" observer after Admin Center is clicked.
          if (ranAC) {
            return;
          }
          let node = mutation.addedNodes[0];
          if (node?.children != null && node.children.length > 0) {
            // Open settings dropdown.
            const SettingsButtonID = "O365_MainLink_Settings";
            if (!ranSettings && node.children[0].id == SettingsButtonID) {
              ranSettings = true;
              document.getElementById(SettingsButtonID).click();
            } // Open Admin Center (in new tab) and close current tab.
            // Also set a cookie with the SAS link for use in the Admin Center.
            else {
              let adminCenter = document.getElementById("AdminCenter")
              if (adminCenter) {
                ranAC = true;
                setCookie(LINK_LABEL, Link)
                adminCenter.click();
                window.close();
              }
            }
          }
        });
      });
      observer.observe(document.documentElement, {
        childList: true,
        subtree: true
      });
    } // BC Admin Center
    else {
      // Get SAS link from cookie.
      const Link = getCookies()[LINK_LABEL];
      // If no link found, exit for normal use.
      if (!Link || Link.length == 0) {
        return;
      }
      // Environments
      findClickWait("ms-Button ms-Button--action ms-Button--command", "Environments", 0);
      // Wait for loading of environments.
      let run = false;
      const EnvListClassName = "ms-List-page"
      let observer = new MutationObserver(function(mutations) {
        mutations.forEach(async function(mutation) {
          let node = mutation.addedNodes[0];
          //console.log(node);
          if (node?.children != null) {
            if (node.className == EnvListClassName) {
              // First environment
              let envList = getXthElementByClassName(EnvListClassName);
              let env = envList.children[ENV_IDX].children[0].children[0].children[0].children[0].children[0];
              env.click();
              await sleep(T);
              // Database dropdown
              await findClickWait("ms-Button ms-Button--commandBar ms-Button--hasMenu", "Database", 0.5*T);
              // Create export
              await findClickWait("ms-ContextualMenu-link", "Create database export");
            } else if (node.className.startsWith("ms-Layer ms-Layer--fixed")) {
              // Insert link
              let sasTxt = getElementByClassNameAndTitle("ms-TextField-field", "SAS URI from Azure");
              if (sasTxt) {
                let decLink = decodeURIComponent(Link);
                await sleep(T);
                if (await okCancelDialog(PASTE_SAS_URI_MSG)) {
                  const inputHandler = async function(e) {
                    if (e.target.value == decLink) {
                      await sleep(T);
                      await findClickWait("ms-Button ms-Button--primary", "Create", 5*T, true);
                      window.close();
                    }
                  }
                  sasTxt.addEventListener("input", inputHandler);
                  navigator.clipboard.writeText(decLink);
                  sasTxt.focus();
                }
              }
            }
          }
        });
      });
      observer.observe(document.documentElement, {
        childList: true,
        subtree: true
      });
    }
  }
})();