about summary refs log tree commit diff stats
path: root/modules/by-name/un/unison
diff options
context:
space:
mode:
Diffstat (limited to '')
-rw-r--r--modules/by-name/un/unison/module.nix92
-rw-r--r--modules/by-name/un/unison/shellScript.nix95
2 files changed, 187 insertions, 0 deletions
diff --git a/modules/by-name/un/unison/module.nix b/modules/by-name/un/unison/module.nix
new file mode 100644
index 00000000..baf92b02
--- /dev/null
+++ b/modules/by-name/un/unison/module.nix
@@ -0,0 +1,92 @@
+{
+  lib,
+  config,
+  pkgs,
+  sysLib,
+  ...
+}: let
+  cfg = config.soispha.services.unison;
+
+  script = import ./shellScript.nix {inherit sysLib lib pkgs cfg;};
+in {
+  options.soispha.services.unison = let
+    homePath = lib.types.strMatching "^~.*";
+  in {
+    enable = lib.mkEnableOption "a unison home sync script";
+
+    dataDir = lib.mkOption {
+      type = lib.types.path;
+      description = ''
+        This directory is used by unison to store it's data.
+      '';
+    };
+
+    foreign = {
+      address = lib.mkOption {
+        type = lib.types.str;
+        description = ''
+          The address to contact via ssh, when syncing.
+        '';
+      };
+      userName = lib.mkOption {
+        type = lib.types.str;
+        description = ''
+          The user name to try to login with at the foreign host.
+        '';
+      };
+    };
+
+    userSourceDir = lib.mkOption {
+      description = ''
+        The directory to replace the `~` in the relative user paths with.
+        If using `impermanence`, this should be the path to the persistent home directory.
+      '';
+    };
+
+    pathsToIgnore = lib.mkOption {
+      type = lib.types.listOf homePath;
+      default = [];
+      description = ''
+        A list of the paths that should not be synced.
+        Beware that this applies not only to this path, but also to all paths under it.
+      '';
+    };
+    pathsToSync = lib.mkOption {
+      type = lib.types.listOf homePath;
+      default = [];
+      description = ''
+        A list of the paths that should be synced.
+        Beware that this applies not only to this path, but also to all paths under it.
+      '';
+    };
+
+    unisonOptions = lib.mkOption {
+      internal = true;
+      default = {
+        sshcmd = "ssh";
+        ui = "text";
+        auto = "true";
+        # This is a trap, thanks to the HM links
+        # TODO: Auto-ignore all `home.file` paths <2024-10-24>
+        links = "false";
+
+        backupdir = "${cfg.dataDir}/backups";
+        backuploc = "central";
+      };
+      type = lib.types.attrsOf lib.types.str;
+      description = "The options passed to every unison call.";
+    };
+  };
+
+  config = lib.mkIf cfg.enable {
+    home-manager.users.soispha = {
+      home.sessionVariables = {
+        UNISON = cfg.dataDir;
+      };
+      home.packages = [
+        pkgs.unison
+        script
+      ];
+    };
+  };
+}
diff --git a/modules/by-name/un/unison/shellScript.nix b/modules/by-name/un/unison/shellScript.nix
new file mode 100644
index 00000000..5ff0c219
--- /dev/null
+++ b/modules/by-name/un/unison/shellScript.nix
@@ -0,0 +1,95 @@
+{
+  sysLib,
+  lib,
+  pkgs,
+  cfg,
+}: let
+  esa = lib.strings.escapeShellArg;
+
+  expandHomePath = path:
+    if lib.strings.hasPrefix "~" path
+    then "${cfg.userSourceDir}${lib.strings.removePrefix "~" path}"
+    else
+      builtins.throw
+      ''
+        BUG: Every pathname needs to start with a '~'.
+        This should have been checked by the NixOS module system?
+      '';
+
+  getIgnored = paths_to_ignore: path:
+    serialiseArgs {
+      ignore =
+        builtins.filter (x: x != null) (builtins.map (getIgnoredSingle path) paths_to_ignore);
+    };
+
+  getIgnoredSingle = path: pathToIgnore: let
+    clean_path_to_ignore = expandHomePath pathToIgnore;
+    commonPath = builtins.substring 0 (builtins.stringLength path) clean_path_to_ignore;
+  in
+    if commonPath == path
+    then let
+      preFinalPath =
+        builtins.substring (builtins.stringLength commonPath)
+        (builtins.stringLength clean_path_to_ignore)
+        clean_path_to_ignore;
+      finalPath =
+        if lib.strings.hasPrefix "/" preFinalPath
+        then lib.strings.removePrefix "/" preFinalPath
+        else preFinalPath;
+    in "BelowPath ${finalPath}"
+    else null;
+
+  serialiseArg = key: val:
+    if builtins.typeOf val == "string"
+    then esa "-${key}=${lib.strings.escape ["="] val}"
+    else if builtins.typeOf val == "list"
+    then lib.strings.concatStringsSep " " (builtins.map (serialiseArg key) val)
+    else builtins.throw "Unsupported type: ${builtins.typeOf val}";
+
+  serialiseArgs = args:
+    lib.strings.concatStringsSep " " (
+      lib.attrsets.mapAttrsToList
+      serialiseArg
+      args
+    );
+
+  mkScriptLine = pathname: let
+    path =
+      expandHomePath pathname;
+  in
+    lib.strings.concatStringsSep " " [
+      "unison"
+      "${serialiseArgs cfg.unisonOptions}"
+      "$EXTRA_OPTIONS"
+      "${getIgnored cfg.pathsToIgnore path}"
+      "${esa path}"
+      (esa "ssh://${cfg.foreign.userName}@${cfg.foreign.address}/${path}")
+    ];
+
+  script = lib.strings.concatStringsSep "\n" (builtins.map mkScriptLine cfg.pathsToSync);
+in
+  sysLib.writeShellScript {
+    name = "unison-sync";
+    src = builtins.toFile "unison-backup" (''
+        #!/usr/bin/env dash
+
+        # shellcheck source=/dev/null
+        SHELL_LIBRARY_VERSION="2.1.2" . %SHELL_LIBRARY_PATH
+
+        export UNISON=${esa cfg.dataDir};
+
+        if [ "$1" = "links" ]; then
+          shift 1;
+          EXTRA_OPTIONS="-links=true";
+        fi
+        EXTRA_OPTIONS="$EXTRA_OPTIONS $*"
+      ''
+      + script);
+
+    dependencies = with pkgs; [
+      unison
+      openssh # needed to connect to the other server
+      less # needed to show diffs
+      diffutils # needed to compute diffs
+    ];
+  }