diff --git a/flake.nix b/flake.nix index 6ff7cdb..20bf7b8 100644 --- a/flake.nix +++ b/flake.nix @@ -30,20 +30,20 @@ zjstatus.url = "github:dj95/zjstatus"; }; - outputs = inputs @ {flake-parts, ...}: - let - loadOverlays = path: with builtins; + outputs = inputs @ {flake-parts, ...}: let + loadOverlays = path: + with builtins; map (n: import (path + ("/" + n))) - (filter (n: match ".*\\.nix" n != null) - (attrNames (readDir path))); - in + (filter (n: match ".*\\.nix" n != null) + (attrNames (readDir path))); + in flake-parts.lib.mkFlake {inherit inputs;} ( let constants = import ./lib/constants.nix; user = constants.user; darwinHosts = builtins.attrNames (builtins.readDir ./hosts/darwin); nixosHosts = builtins.attrNames (builtins.readDir ./hosts/nixos); - + commonOverlays = loadOverlays ./overlays; darwinOverlays = loadOverlays ./overlays/darwin; in { @@ -53,12 +53,7 @@ ]; flake.darwinConfigurations = inputs.nixpkgs.lib.genAttrs darwinHosts ( - hostname: let - syncthingOverlay = builtins.filter (o: o ? darwinSyncthingModule) darwinOverlays; - darwinModules = if syncthingOverlay != [] - then [((builtins.head syncthingOverlay) null {}).darwinSyncthingModule] - else []; - in + hostname: inputs.darwin.lib.darwinSystem { system = "aarch64-darwin"; specialArgs = @@ -69,29 +64,29 @@ modules = [ inputs.home-manager.darwinModules.home-manager inputs.nix-homebrew.darwinModules.nix-homebrew - ] - ++ darwinModules - ++ [ - { - nixpkgs.overlays = commonOverlays ++ darwinOverlays ++ [ - (final: prev: { - zjstatus = inputs.zjstatus.packages.${prev.system}.default; - }) - ]; + { + nixpkgs.overlays = + commonOverlays + ++ darwinOverlays + ++ [ + (final: prev: { + zjstatus = inputs.zjstatus.packages.${prev.system}.default; + }) + ]; - nix-homebrew = { - inherit user; - enable = true; - taps = { - "homebrew/homebrew-core" = inputs.homebrew-core; - "homebrew/homebrew-cask" = inputs.homebrew-cask; - "cameroncooke/axe" = inputs.homebrew-axe; + nix-homebrew = { + inherit user; + enable = true; + taps = { + "homebrew/homebrew-core" = inputs.homebrew-core; + "homebrew/homebrew-cask" = inputs.homebrew-cask; + "cameroncooke/axe" = inputs.homebrew-axe; + }; + mutableTaps = true; }; - mutableTaps = true; - }; - } - ./hosts/darwin/${hostname} - ]; + } + ./hosts/darwin/${hostname} + ]; } ); @@ -107,11 +102,13 @@ modules = [ inputs.home-manager.nixosModules.home-manager { - nixpkgs.overlays = commonOverlays ++ [ - (final: prev: { - zjstatus = inputs.zjstatus.packages.${prev.system}.default; - }) - ]; + nixpkgs.overlays = + commonOverlays + ++ [ + (final: prev: { + zjstatus = inputs.zjstatus.packages.${prev.system}.default; + }) + ]; } ./hosts/nixos/${hostname} ]; diff --git a/hosts/darwin/chidi/default.nix b/hosts/darwin/chidi/default.nix index 781c3f5..906b67d 100644 --- a/hosts/darwin/chidi/default.nix +++ b/hosts/darwin/chidi/default.nix @@ -13,9 +13,7 @@ networking.hostName = "chidi"; networking.computerName = "Chidi"; - nixpkgs.overlays = [ - (import ../../../overlays/postgresql-darwin.nix) - ]; + services.postgresql = { enable = true; diff --git a/modules/darwin/default.nix b/modules/darwin/default.nix index 4cba3d7..0e3afae 100644 --- a/modules/darwin/default.nix +++ b/modules/darwin/default.nix @@ -13,6 +13,7 @@ ../tailscale.nix ./dock ./homebrew.nix + ./syncthing.nix ./system.nix sops-nix.darwinModules.sops ]; diff --git a/modules/darwin/syncthing.nix b/modules/darwin/syncthing.nix new file mode 100644 index 0000000..0a5eb30 --- /dev/null +++ b/modules/darwin/syncthing.nix @@ -0,0 +1,396 @@ +{ + config, + lib, + pkgs, + ... +}: +with lib; let + cfg = config.services.syncthing; + settingsFormat = pkgs.formats.json {}; + cleanedConfig = converge (filterAttrsRecursive (_: v: v != null && v != {})) cfg.settings; + + isUnixGui = (builtins.substring 0 1 cfg.guiAddress) == "/"; + + curlAddressArgs = path: + if isUnixGui + then "--unix-socket ${cfg.guiAddress} http://.${path}" + else "${cfg.guiAddress}${path}"; + + devices = mapAttrsToList (_: device: device // {deviceID = device.id;}) cfg.settings.devices; + anyAutoAccept = builtins.any (dev: dev.autoAcceptFolders) devices; + + folders = mapAttrsToList (_: folder: + folder + // { + devices = let + folderDevices = folder.devices; + in + map ( + device: + if builtins.isString device + then {deviceId = cfg.settings.devices.${device}.id;} + else if builtins.isAttrs device + then {deviceId = cfg.settings.devices.${device.name}.id;} // device + else throw "Invalid type for devices in folder; expected list or attrset." + ) + folderDevices; + }) (filterAttrs (_: folder: folder.enable) cfg.settings.folders); + + jq = "${pkgs.jq}/bin/jq"; + updateConfig = pkgs.writers.writeBash "merge-syncthing-config" ( + '' + set -efu + umask 0077 + + curl() { + while + ! ${pkgs.libxml2}/bin/xmllint \ + --xpath 'string(configuration/gui/apikey)' \ + ${cfg.configDir}/config.xml \ + >"$TMPDIR/api_key" + do sleep 1; done + (printf "X-API-Key: "; cat "$TMPDIR/api_key") >"$TMPDIR/headers" + ${pkgs.curl}/bin/curl -sSLk -H "@$TMPDIR/headers" \ + --retry 1000 --retry-delay 1 --retry-all-errors \ + "$@" + } + '' + + (lib.pipe { + devs = { + new_conf_IDs = map (v: v.id) devices; + GET_IdAttrName = "deviceID"; + override = cfg.overrideDevices; + conf = devices; + baseAddress = curlAddressArgs "/rest/config/devices"; + }; + dirs = { + new_conf_IDs = map (v: v.id) folders; + GET_IdAttrName = "id"; + override = cfg.overrideFolders; + conf = folders; + baseAddress = curlAddressArgs "/rest/config/folders"; + }; + } [ + (mapAttrs ( + conf_type: s: + lib.pipe s.conf [ + (map ( + new_cfg: let + jsonPreSecretsFile = pkgs.writeTextFile { + name = "${conf_type}-${new_cfg.id}-conf-pre-secrets.json"; + text = builtins.toJSON new_cfg; + }; + injectSecretsJqCmd = + { + "devs" = "${jq} ."; + "dirs" = let + folder = new_cfg; + devicesWithSecrets = lib.pipe folder.devices [ + (lib.filter (device: (builtins.isAttrs device) && device ? encryptionPasswordFile)) + (map (device: { + deviceId = device.deviceId; + variableName = "secret_${builtins.hashString "sha256" device.encryptionPasswordFile}"; + secretPath = device.encryptionPasswordFile; + })) + ]; + jqUpdates = + map (device: '' + .devices[] |= ( + if .deviceId == "${device.deviceId}" then + del(.encryptionPasswordFile) | + .encryptionPassword = ''$${device.variableName} + else + . + end + ) + '') + devicesWithSecrets; + jqRawFiles = map (device: "--rawfile ${device.variableName} ${lib.escapeShellArg device.secretPath}") devicesWithSecrets; + in "${jq} ${lib.concatStringsSep " " jqRawFiles} ${lib.escapeShellArg (lib.concatStringsSep "|" (["."] ++ jqUpdates))}"; + }.${ + conf_type + }; + in '' + ${injectSecretsJqCmd} ${jsonPreSecretsFile} | curl --json @- -X POST ${s.baseAddress} + '' + )) + (lib.concatStringsSep "\n") + ] + + lib.optionalString s.override '' + stale_${conf_type}_ids="$(curl -X GET ${s.baseAddress} | ${jq} \ + --argjson new_ids ${lib.escapeShellArg (builtins.toJSON s.new_conf_IDs)} \ + --raw-output \ + '[.[].${s.GET_IdAttrName}] - $new_ids | .[]' + )" + for id in ''${stale_${conf_type}_ids}; do + >&2 echo "Deleting stale device: $id" + curl -X DELETE ${s.baseAddress}/$id + done + '' + )) + builtins.attrValues + (lib.concatStringsSep "\n") + ]) + + (lib.pipe cleanedConfig [ + builtins.attrNames + (lib.subtractLists ["folders" "devices"]) + (map (subOption: '' + curl -X PUT -d ${lib.escapeShellArg (builtins.toJSON cleanedConfig.${subOption})} ${curlAddressArgs "/rest/config/${subOption}"} + '')) + (lib.concatStringsSep "\n") + ]) + + '' + if curl ${curlAddressArgs "/rest/config/restart-required"} | + ${jq} -e .requiresRestart > /dev/null; then + curl -X POST ${curlAddressArgs "/rest/system/restart"} + fi + '' + ); +in { + options = { + services.syncthing = { + enable = mkEnableOption "Syncthing, a self-hosted open-source alternative to Dropbox and Bittorrent Sync"; + + cert = mkOption { + type = types.nullOr types.str; + default = null; + description = "Path to the cert.pem file, which will be copied into Syncthing's configDir."; + }; + + key = mkOption { + type = types.nullOr types.str; + default = null; + description = "Path to the key.pem file, which will be copied into Syncthing's configDir."; + }; + + overrideDevices = mkOption { + type = types.bool; + default = true; + description = "Whether to delete the devices which are not configured via the devices option."; + }; + + overrideFolders = mkOption { + type = types.bool; + default = !anyAutoAccept; + description = "Whether to delete the folders which are not configured via the folders option."; + }; + + settings = mkOption { + type = types.submodule { + freeformType = settingsFormat.type; + options = { + options = mkOption { + default = {}; + description = "The options element contains all other global configuration options"; + type = types.submodule { + freeformType = settingsFormat.type; + options = { + localAnnounceEnabled = mkOption { + type = types.nullOr types.bool; + default = null; + description = "Whether to send announcements to the local LAN."; + }; + globalAnnounceEnabled = mkOption { + type = types.nullOr types.bool; + default = null; + description = "Whether to send announcements to the global discovery servers."; + }; + relaysEnabled = mkOption { + type = types.nullOr types.bool; + default = null; + description = "When true, relays will be connected to and potentially used for device to device connections."; + }; + urAccepted = mkOption { + type = types.nullOr types.int; + default = null; + description = "Whether the user has accepted to submit anonymous usage data."; + }; + }; + }; + }; + + devices = mkOption { + default = {}; + description = "Peers/devices which Syncthing should communicate with."; + type = types.attrsOf (types.submodule ({name, ...}: { + freeformType = settingsFormat.type; + options = { + name = mkOption { + type = types.str; + default = name; + description = "The name of the device."; + }; + id = mkOption { + type = types.str; + description = "The device ID."; + }; + autoAcceptFolders = mkOption { + type = types.bool; + default = false; + description = "Automatically create or share folders that this device advertises at the default path."; + }; + }; + })); + }; + + folders = mkOption { + default = {}; + description = "Folders which should be shared by Syncthing."; + type = types.attrsOf (types.submodule ({name, ...}: { + freeformType = settingsFormat.type; + options = { + enable = mkOption { + type = types.bool; + default = true; + description = "Whether to share this folder."; + }; + path = mkOption { + type = types.str; + default = name; + description = "The path to the folder which should be shared."; + }; + id = mkOption { + type = types.str; + default = name; + description = "The ID of the folder. Must be the same on all devices."; + }; + label = mkOption { + type = types.str; + default = name; + description = "The label of the folder."; + }; + type = mkOption { + type = types.enum ["sendreceive" "sendonly" "receiveonly" "receiveencrypted"]; + default = "sendreceive"; + description = "Controls how the folder is handled by Syncthing."; + }; + devices = mkOption { + type = types.listOf (types.oneOf [ + types.str + (types.submodule { + freeformType = settingsFormat.type; + options = { + name = mkOption { + type = types.str; + description = "The name of a device defined in the devices option."; + }; + encryptionPasswordFile = mkOption { + type = types.nullOr types.path; + default = null; + description = "Path to encryption password file."; + }; + }; + }) + ]); + default = []; + description = "The devices this folder should be shared with."; + }; + }; + })); + }; + }; + }; + default = {}; + description = "Extra configuration options for Syncthing."; + }; + + guiAddress = mkOption { + type = types.str; + default = "127.0.0.1:8384"; + description = "The address to serve the web interface at."; + }; + + user = mkOption { + type = types.str; + default = "syncthing"; + description = "The user to run Syncthing as."; + }; + + group = mkOption { + type = types.str; + default = "syncthing"; + description = "The group to run Syncthing under."; + }; + + dataDir = mkOption { + type = types.path; + default = "/var/lib/syncthing"; + description = "The path where synchronised directories will exist."; + }; + + configDir = mkOption { + type = types.path; + default = cfg.dataDir + "/.config/syncthing"; + description = "The path where the settings and keys will exist."; + }; + + openDefaultPorts = mkOption { + type = types.bool; + default = false; + description = "Whether to open the default ports in the firewall (not applicable on Darwin)."; + }; + + package = mkPackageOption pkgs "syncthing" {}; + }; + }; + + config = mkIf cfg.enable { + assertions = [ + { + assertion = !(cfg.overrideFolders && anyAutoAccept); + message = "services.syncthing.overrideFolders will delete auto-accepted folders from the configuration, creating path conflicts."; + } + ]; + + environment.systemPackages = [cfg.package]; + + launchd.user.agents.syncthing = { + serviceConfig = { + ProgramArguments = [ + "${cfg.package}/bin/syncthing" + "-no-browser" + "-gui-address=${ + if isUnixGui + then "unix://" + else "" + }${cfg.guiAddress}" + "-config=${cfg.configDir}" + "-data=${cfg.configDir}" + ]; + EnvironmentVariables = { + STNORESTART = "yes"; + STNOUPGRADE = "yes"; + }; + KeepAlive = true; + RunAtLoad = true; + ProcessType = "Background"; + StandardOutPath = "${cfg.configDir}/syncthing.log"; + StandardErrorPath = "${cfg.configDir}/syncthing.log"; + }; + }; + + launchd.user.agents.syncthing-init = mkIf (cleanedConfig != {}) { + serviceConfig = { + ProgramArguments = ["${updateConfig}"]; + RunAtLoad = true; + KeepAlive = false; + ProcessType = "Background"; + StandardOutPath = "${cfg.configDir}/syncthing-init.log"; + StandardErrorPath = "${cfg.configDir}/syncthing-init.log"; + }; + }; + + system.activationScripts.syncthing = mkIf (cfg.cert != null || cfg.key != null) '' + echo "Setting up Syncthing certificates..." + mkdir -p ${cfg.configDir} + ${optionalString (cfg.cert != null) '' + cp ${toString cfg.cert} ${cfg.configDir}/cert.pem + chmod 644 ${cfg.configDir}/cert.pem + ''} + ${optionalString (cfg.key != null) '' + cp ${toString cfg.key} ${cfg.configDir}/key.pem + chmod 600 ${cfg.configDir}/key.pem + ''} + ''; + }; +} \ No newline at end of file diff --git a/modules/syncthing.nix b/modules/syncthing.nix index bea9f2a..31af060 100644 --- a/modules/syncthing.nix +++ b/modules/syncthing.nix @@ -4,25 +4,16 @@ ... }: let isDarwin = pkgs.stdenv.isDarwin; - - platformConfig = - if isDarwin - then { - homeDir = "/Users/${user}"; - group = "staff"; - } - else { - homeDir = "/home/${user}"; - group = "users"; - }; + homeDir = if isDarwin then "/Users/${user}" else "/home/${user}"; + group = if isDarwin then "staff" else "users"; in { services.syncthing = { enable = true; openDefaultPorts = !isDarwin; - dataDir = "${platformConfig.homeDir}/.local/share/syncthing"; - configDir = "${platformConfig.homeDir}/.config/syncthing"; + dataDir = "${homeDir}/.local/share/syncthing"; + configDir = "${homeDir}/.config/syncthing"; user = "${user}"; - group = platformConfig.group; + group = group; guiAddress = "0.0.0.0:8384"; overrideFolders = true; overrideDevices = true; @@ -45,7 +36,7 @@ in { folders = { "nixos-config" = { - path = "${platformConfig.homeDir}/nixos-config"; + path = "${homeDir}/nixos-config"; devices = ["tahani" "jason" "chidi"]; }; }; diff --git a/overlays/darwin/syncthing.nix b/overlays/darwin/syncthing.nix deleted file mode 100644 index 0027eff..0000000 --- a/overlays/darwin/syncthing.nix +++ /dev/null @@ -1,400 +0,0 @@ -final: prev: { - darwinSyncthingModule = { - config, - lib, - pkgs, - ... - }: - with lib; let - cfg = config.services.syncthing; - defaultUser = "syncthing"; - defaultGroup = defaultUser; - settingsFormat = pkgs.formats.json {}; - cleanedConfig = converge (filterAttrsRecursive (_: v: v != null && v != {})) cfg.settings; - - isUnixGui = (builtins.substring 0 1 cfg.guiAddress) == "/"; - - curlAddressArgs = path: - if isUnixGui - then "--unix-socket ${cfg.guiAddress} http://.${path}" - else "${cfg.guiAddress}${path}"; - - devices = mapAttrsToList (_: device: device // {deviceID = device.id;}) cfg.settings.devices; - anyAutoAccept = builtins.any (dev: dev.autoAcceptFolders) devices; - - folders = mapAttrsToList (_: folder: - folder - // { - devices = let - folderDevices = folder.devices; - in - map ( - device: - if builtins.isString device - then {deviceId = cfg.settings.devices.${device}.id;} - else if builtins.isAttrs device - then {deviceId = cfg.settings.devices.${device.name}.id;} // device - else throw "Invalid type for devices in folder; expected list or attrset." - ) - folderDevices; - }) (filterAttrs (_: folder: folder.enable) cfg.settings.folders); - - jq = "${pkgs.jq}/bin/jq"; - updateConfig = pkgs.writers.writeBash "merge-syncthing-config" ( - '' - set -efu - umask 0077 - - curl() { - while - ! ${pkgs.libxml2}/bin/xmllint \ - --xpath 'string(configuration/gui/apikey)' \ - ${cfg.configDir}/config.xml \ - >"$TMPDIR/api_key" - do sleep 1; done - (printf "X-API-Key: "; cat "$TMPDIR/api_key") >"$TMPDIR/headers" - ${pkgs.curl}/bin/curl -sSLk -H "@$TMPDIR/headers" \ - --retry 1000 --retry-delay 1 --retry-all-errors \ - "$@" - } - '' - + (lib.pipe { - devs = { - new_conf_IDs = map (v: v.id) devices; - GET_IdAttrName = "deviceID"; - override = cfg.overrideDevices; - conf = devices; - baseAddress = curlAddressArgs "/rest/config/devices"; - }; - dirs = { - new_conf_IDs = map (v: v.id) folders; - GET_IdAttrName = "id"; - override = cfg.overrideFolders; - conf = folders; - baseAddress = curlAddressArgs "/rest/config/folders"; - }; - } [ - (mapAttrs ( - conf_type: s: - lib.pipe s.conf [ - (map ( - new_cfg: let - jsonPreSecretsFile = pkgs.writeTextFile { - name = "${conf_type}-${new_cfg.id}-conf-pre-secrets.json"; - text = builtins.toJSON new_cfg; - }; - injectSecretsJqCmd = - { - "devs" = "${jq} ."; - "dirs" = let - folder = new_cfg; - devicesWithSecrets = lib.pipe folder.devices [ - (lib.filter (device: (builtins.isAttrs device) && device ? encryptionPasswordFile)) - (map (device: { - deviceId = device.deviceId; - variableName = "secret_${builtins.hashString "sha256" device.encryptionPasswordFile}"; - secretPath = device.encryptionPasswordFile; - })) - ]; - jqUpdates = - map (device: '' - .devices[] |= ( - if .deviceId == "${device.deviceId}" then - del(.encryptionPasswordFile) | - .encryptionPassword = ''$${device.variableName} - else - . - end - ) - '') - devicesWithSecrets; - jqRawFiles = map (device: "--rawfile ${device.variableName} ${lib.escapeShellArg device.secretPath}") devicesWithSecrets; - in "${jq} ${lib.concatStringsSep " " jqRawFiles} ${lib.escapeShellArg (lib.concatStringsSep "|" (["."] ++ jqUpdates))}"; - }.${ - conf_type - }; - in '' - ${injectSecretsJqCmd} ${jsonPreSecretsFile} | curl --json @- -X POST ${s.baseAddress} - '' - )) - (lib.concatStringsSep "\n") - ] - + lib.optionalString s.override '' - stale_${conf_type}_ids="$(curl -X GET ${s.baseAddress} | ${jq} \ - --argjson new_ids ${lib.escapeShellArg (builtins.toJSON s.new_conf_IDs)} \ - --raw-output \ - '[.[].${s.GET_IdAttrName}] - $new_ids | .[]' - )" - for id in ''${stale_${conf_type}_ids}; do - >&2 echo "Deleting stale device: $id" - curl -X DELETE ${s.baseAddress}/$id - done - '' - )) - builtins.attrValues - (lib.concatStringsSep "\n") - ]) - + (lib.pipe cleanedConfig [ - builtins.attrNames - (lib.subtractLists ["folders" "devices"]) - (map (subOption: '' - curl -X PUT -d ${lib.escapeShellArg (builtins.toJSON cleanedConfig.${subOption})} ${curlAddressArgs "/rest/config/${subOption}"} - '')) - (lib.concatStringsSep "\n") - ]) - + '' - if curl ${curlAddressArgs "/rest/config/restart-required"} | - ${jq} -e .requiresRestart > /dev/null; then - curl -X POST ${curlAddressArgs "/rest/system/restart"} - fi - '' - ); - in { - options = { - services.syncthing = { - enable = mkEnableOption "Syncthing, a self-hosted open-source alternative to Dropbox and Bittorrent Sync"; - - cert = mkOption { - type = types.nullOr types.str; - default = null; - description = "Path to the cert.pem file, which will be copied into Syncthing's configDir."; - }; - - key = mkOption { - type = types.nullOr types.str; - default = null; - description = "Path to the key.pem file, which will be copied into Syncthing's configDir."; - }; - - overrideDevices = mkOption { - type = types.bool; - default = true; - description = "Whether to delete the devices which are not configured via the devices option."; - }; - - overrideFolders = mkOption { - type = types.bool; - default = !anyAutoAccept; - description = "Whether to delete the folders which are not configured via the folders option."; - }; - - settings = mkOption { - type = types.submodule { - freeformType = settingsFormat.type; - options = { - options = mkOption { - default = {}; - description = "The options element contains all other global configuration options"; - type = types.submodule { - freeformType = settingsFormat.type; - options = { - localAnnounceEnabled = mkOption { - type = types.nullOr types.bool; - default = null; - description = "Whether to send announcements to the local LAN."; - }; - globalAnnounceEnabled = mkOption { - type = types.nullOr types.bool; - default = null; - description = "Whether to send announcements to the global discovery servers."; - }; - relaysEnabled = mkOption { - type = types.nullOr types.bool; - default = null; - description = "When true, relays will be connected to and potentially used for device to device connections."; - }; - urAccepted = mkOption { - type = types.nullOr types.int; - default = null; - description = "Whether the user has accepted to submit anonymous usage data."; - }; - }; - }; - }; - - devices = mkOption { - default = {}; - description = "Peers/devices which Syncthing should communicate with."; - type = types.attrsOf (types.submodule ({name, ...}: { - freeformType = settingsFormat.type; - options = { - name = mkOption { - type = types.str; - default = name; - description = "The name of the device."; - }; - id = mkOption { - type = types.str; - description = "The device ID."; - }; - autoAcceptFolders = mkOption { - type = types.bool; - default = false; - description = "Automatically create or share folders that this device advertises at the default path."; - }; - }; - })); - }; - - folders = mkOption { - default = {}; - description = "Folders which should be shared by Syncthing."; - type = types.attrsOf (types.submodule ({name, ...}: { - freeformType = settingsFormat.type; - options = { - enable = mkOption { - type = types.bool; - default = true; - description = "Whether to share this folder."; - }; - path = mkOption { - type = types.str; - default = name; - description = "The path to the folder which should be shared."; - }; - id = mkOption { - type = types.str; - default = name; - description = "The ID of the folder. Must be the same on all devices."; - }; - label = mkOption { - type = types.str; - default = name; - description = "The label of the folder."; - }; - type = mkOption { - type = types.enum ["sendreceive" "sendonly" "receiveonly" "receiveencrypted"]; - default = "sendreceive"; - description = "Controls how the folder is handled by Syncthing."; - }; - devices = mkOption { - type = types.listOf (types.oneOf [ - types.str - (types.submodule { - freeformType = settingsFormat.type; - options = { - name = mkOption { - type = types.str; - description = "The name of a device defined in the devices option."; - }; - encryptionPasswordFile = mkOption { - type = types.nullOr types.path; - default = null; - description = "Path to encryption password file."; - }; - }; - }) - ]); - default = []; - description = "The devices this folder should be shared with."; - }; - }; - })); - }; - }; - }; - default = {}; - description = "Extra configuration options for Syncthing."; - }; - - guiAddress = mkOption { - type = types.str; - default = "127.0.0.1:8384"; - description = "The address to serve the web interface at."; - }; - - user = mkOption { - type = types.str; - default = defaultUser; - description = "The user to run Syncthing as."; - }; - - group = mkOption { - type = types.str; - default = defaultGroup; - description = "The group to run Syncthing under."; - }; - - dataDir = mkOption { - type = types.path; - default = "/var/lib/syncthing"; - description = "The path where synchronised directories will exist."; - }; - - configDir = mkOption { - type = types.path; - default = cfg.dataDir + "/.config/syncthing"; - description = "The path where the settings and keys will exist."; - }; - - openDefaultPorts = mkOption { - type = types.bool; - default = false; - description = "Whether to open the default ports in the firewall (not applicable on Darwin)."; - }; - - package = mkPackageOption pkgs "syncthing" {}; - }; - }; - - config = mkIf cfg.enable { - assertions = [ - { - assertion = !(cfg.overrideFolders && anyAutoAccept); - message = "services.syncthing.overrideFolders will delete auto-accepted folders from the configuration, creating path conflicts."; - } - ]; - - environment.systemPackages = [cfg.package]; - - launchd.user.agents.syncthing = { - serviceConfig = { - ProgramArguments = [ - "${cfg.package}/bin/syncthing" - "-no-browser" - "-gui-address=${ - if isUnixGui - then "unix://" - else "" - }${cfg.guiAddress}" - "-config=${cfg.configDir}" - "-data=${cfg.configDir}" - ]; - EnvironmentVariables = { - STNORESTART = "yes"; - STNOUPGRADE = "yes"; - }; - KeepAlive = true; - RunAtLoad = true; - ProcessType = "Background"; - StandardOutPath = "${cfg.configDir}/syncthing.log"; - StandardErrorPath = "${cfg.configDir}/syncthing.log"; - }; - }; - - launchd.user.agents.syncthing-init = mkIf (cleanedConfig != {}) { - serviceConfig = { - ProgramArguments = ["${updateConfig}"]; - RunAtLoad = true; - KeepAlive = false; - ProcessType = "Background"; - StandardOutPath = "${cfg.configDir}/syncthing-init.log"; - StandardErrorPath = "${cfg.configDir}/syncthing-init.log"; - }; - }; - - system.activationScripts.syncthing = mkIf (cfg.cert != null || cfg.key != null) '' - echo "Setting up Syncthing certificates..." - mkdir -p ${cfg.configDir} - ${optionalString (cfg.cert != null) '' - cp ${toString cfg.cert} ${cfg.configDir}/cert.pem - chmod 644 ${cfg.configDir}/cert.pem - ''} - ${optionalString (cfg.key != null) '' - cp ${toString cfg.key} ${cfg.configDir}/key.pem - chmod 600 ${cfg.configDir}/key.pem - ''} - ''; - }; - }; -}