repo_description: Cross-platform backup tool for Windows, macOS & Linux with fast, incremental backups, client-side end-to-end encryption, compression and data deduplication. CLI and GUI included.
repo_topics: backup cloud deduplication encryption google-cloud-storage
code: import { ipcMain } from "electron";
  const path = await import("path");
  const https = await import("https");
  
  import { defaultServerBinary } from "./utils.js";
  import { spawn } from "child_process";
  import log from "electron-log";
  import { configDir, isPortableConfig } from "./config.js";
  
  let servers = {};
  
  function newServerForRepo(repoID) {
    let runningServerProcess = null;
    let runningServerCertSHA256 = "";
    let runningServerPassword = "";
    let runningServerControlPassword = "";
    let runningServerAddress = "";
    let runningServerCertificate = "";
    let runningServerStatusDetails = {
      startingUp: true,
    };
    let serverLog = [];
  
    const maxLogLines = 100;
  
    return {
      actuateServer() {
        log.info("actuating Server", repoID);
        this.stopServer();
        this.startServer();
      },
  
      startServer() {
        let kopiaPath = defaultServerBinary();
        let args = [];
  
        args.push(
          "server",
          "start",
          "--ui",
          "--tls-print-server-cert",
          "--tls-generate-cert-name=127.0.0.1",
          "--random-password",
          "--random-server-control-password",
          "--tls-generate-cert",
          "--async-repo-connect",
          "--error-notifications=always",
          "--kopiaui-notifications", // will print notification JSON to stderr
          "--shutdown-on-stdin", // shutdown the server when parent dies
          "--address=127.0.0.1:0",
        );
  
        args.push("--config-file", path.resolve(configDir(), repoID + ".config"));
        if (isPortableConfig()) {
          const cacheDir = path.resolve(configDir(), "cache", repoID);
          const logsDir = path.resolve(configDir(), "logs", repoID);
          args.push("--cache-directory", cacheDir);
          args.push("--log-dir", logsDir);
        }
  
        log.info(`spawning ${kopiaPath} ${args.join(" ")}`);
        runningServerProcess = spawn(kopiaPath, args, {});
        this.raiseStatusUpdatedEvent();
  
        runningServerProcess.stdout.on("data", this.appendToLog.bind(this));
        runningServerProcess.stderr.on("data", this.detectServerParam.bind(this));
  
        const p = runningServerProcess;
  
        log.info("starting polling loop");
  
        const statusUpdated = this.raiseStatusUpdatedEvent.bind(this);
  
        const pollInterval = 3000;
  
        function pollOnce() {
          if (
            !runningServerAddress ||
            !runningServerCertificate ||
            !runningServerPassword ||
            !runningServerControlPassword
          ) {
            return;
          }
  
          const req = https.request(
            {
              ca: [runningServerCertificate],
              host: "127.0.0.1",
              port: parseInt(new URL(runningServerAddress).port),
              method: "GET",
              path: "/api/v1/control/status",
              timeout: pollInterval,
              headers: {
                Authorization:
                  "Basic " +
                  Buffer.from(
                    "server-control" + ":" + runningServerControlPassword,
                  ).toString("base64"),
              },
            },
            (resp) => {
              if (resp.statusCode === 200) {
                resp.on("data", (x) => {
                  try {
                    const newDetails = JSON.parse(x);
                    if (
                      JSON.stringify(newDetails) !=
                      JSON.stringify(runningServerStatusDetails)
                    ) {
                      runningServerStatusDetails = newDetails;
                      statusUpdated();
                    }
                  } catch (e) {
                    log.warn("unable to parse status JSON", e);
                  }
                });
              } else {
                log.warn("error fetching status", resp.statusMessage);
              }
            },
          );
          req.on("error", (e) => {
            log.info("error fetching status", e);
          });
          req.end();
        }
  
        const statusPollInterval = setInterval(pollOnce, pollInterval);
  
        runningServerProcess.on("close", (code, signal) => {
          this.appendToLog(
            `child process exited with code ${code} and signal ${signal}`,
          );
          if (runningServerProcess === p) {
            clearInterval(statusPollInterval);
  
            runningServerAddress = "";
            runningServerPassword = "";
            runningServerControlPassword = "";
            runningServerCertSHA256 = "";
            runningServerProcess = null;
            this.raiseStatusUpdatedEvent();
          }
        });
      },
  
      detectServerParam(data) {
        let lines = (data + "").split("\n");
        for (let i = 0; i < lines.length; i++) {
          const p = lines[i].indexOf(": ");
          if (p < 0) {
            continue;
          }
  
          const key = lines[i].substring(0, p);
          const value = lines[i].substring(p + 2);
          switch (key) {
            case "SERVER PASSWORD":
              runningServerPassword = value;
              this.raiseStatusUpdatedEvent();
              break;
  
            case "SERVER CONTROL PASSWORD":
              runningServerControlPassword = value;
              this.raiseStatusUpdatedEvent();
              break;
  
            case "SERVER CERT SHA256":
              runningServerCertSHA256 = value;
              this.raiseStatusUpdatedEvent();
              break;
  
            case "SERVER CERTIFICATE":
              runningServerCertificate = Buffer.from(value, "base64").toString(
                "ascii",
              );
              this.raiseStatusUpdatedEvent();
              break;
  
            case "SERVER ADDRESS":
              runningServerAddress = value;
              this.raiseStatusUpdatedEvent();
              break;
  
            case "NOTIFICATION":
              try {
                this.raiseNotificationEvent(JSON.parse(value));
              } catch (e) {
                log.warn("unable to parse notification JSON", e);
              }
              break;
          }
        }
  
        this.appendToLog(data);
      },
  
      appendToLog(data) {
        const l = serverLog.push(data);
        if (l > maxLogLines) {
          serverLog.splice(0, 1);
        }
  
        ipcMain.emit("logs-updated-event", {
          repoID: repoID,
          logs: serverLog.join(""),
        });
        log.info(`${data}`);
      },
  
      stopServer() {
        if (!runningServerProcess) {
          log.info("stopServer: server not started");
          return;
        }
  
        runningServerProcess.kill();
        runningServerAddress = "";
        runningServerPassword = "";
        runningServerCertSHA256 = "";
        runningServerCertificate = "";
        runningServerProcess = null;
        this.raiseStatusUpdatedEvent();
      },
  
      getServerAddress() {
        return runningServerAddress;
      },
  
      getServerCertSHA256() {
        return runningServerCertSHA256;
      },
  
      getServerPassword() {
        return runningServerPassword;
      },
  
      getServerStatusDetails() {
        return runningServerStatusDetails;
      },
  
      getServerStatus() {
        if (!runningServerProcess) {
          return "Stopped";
        }
  
        if (
          runningServerCertSHA256 &&
          runningServerAddress &&
          runningServerPassword
        ) {
          return "Running";
        }
  
        return "Starting";
      },
  
      raiseStatusUpdatedEvent() {
        const args = {
          repoID: repoID,
          status: this.getServerStatus(),
          serverAddress: this.getServerAddress() || "<pending>",
          serverCertSHA256: this.getServerCertSHA256() || "<pending>",
        };
  
        ipcMain.emit("status-updated-event", args);
      },
  
      raiseNotificationEvent(notification) {
        const args = {
          repoID: repoID,
          notification: notification,
        };
  
        ipcMain.emit("repo-notification-event", args);
      },
    };
  }
  
  ipcMain.on("status-fetch", (event, args) => {
    const repoID = args.repoID;
    const s = servers[repoID];
    if (s) {
      s.raiseStatusUpdatedEvent();
    }
  });
  
  export function serverForRepo(repoID) {
    let s = servers[repoID];
    if (s) {
      return s;
    }
  
    s = newServerForRepo(repoID);
    servers[repoID] = s;
    return s;
  }
  