diff --git a/flake.nix b/flake.nix index 2f56a63..7edbdc7 100644 --- a/flake.nix +++ b/flake.nix @@ -44,6 +44,10 @@ flake.darwinConfigurations = inputs.nixpkgs.lib.genAttrs darwinHosts ( hostname: + let + syncthingOverlay = import ./overlays/syncthing-darwin.nix; + syncthingModule = (syncthingOverlay null { darwin = {}; }).darwinSyncthingModule; + in inputs.darwin.lib.darwinSystem { system = "aarch64-darwin"; specialArgs = @@ -54,8 +58,11 @@ modules = [ inputs.home-manager.darwinModules.home-manager inputs.nix-homebrew.darwinModules.nix-homebrew + syncthingModule { + nixpkgs.overlays = [ syncthingOverlay ]; + nix-homebrew = { inherit user; enable = true; diff --git a/modules/platform/darwin/default.nix b/modules/platform/darwin/default.nix index 85605f8..96b2b6b 100644 --- a/modules/platform/darwin/default.nix +++ b/modules/platform/darwin/default.nix @@ -10,7 +10,6 @@ imports = [ ../../core ../../networking/tailscale.nix - ../../services/syncthing-darwin.nix ../../services/syncthing.nix ./dock ./homebrew.nix diff --git a/modules/services/syncthing-darwin.nix b/modules/services/syncthing-darwin.nix deleted file mode 100644 index 0cb4926..0000000 --- a/modules/services/syncthing-darwin.nix +++ /dev/null @@ -1,386 +0,0 @@ -{ 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 - ''} - ''; - }; -} \ No newline at end of file diff --git a/modules/services/syncthing.nix b/modules/services/syncthing.nix index 9173a35..07db44b 100644 --- a/modules/services/syncthing.nix +++ b/modules/services/syncthing.nix @@ -1,24 +1,20 @@ { user, pkgs, + lib, ... -}: { +}: let + isDarwin = pkgs.stdenv.isDarwin; + isLinux = pkgs.stdenv.isLinux; + homeDir = if isDarwin then "/Users/${user}" else "/home/${user}"; +in { services.syncthing = { enable = true; - openDefaultPorts = pkgs.stdenv.isLinux; - dataDir = - if pkgs.stdenv.isDarwin - then "/Users/${user}/.local/share/syncthing" - else "/home/${user}/.local/share/syncthing"; - configDir = - if pkgs.stdenv.isDarwin - then "/Users/${user}/.config/syncthing" - else "/home/${user}/.config/syncthing"; + openDefaultPorts = isLinux; + dataDir = "${homeDir}/.local/share/syncthing"; + configDir = "${homeDir}/.config/syncthing"; user = "${user}"; - group = - if pkgs.stdenv.isDarwin - then "staff" - else "users"; + group = if isDarwin then "staff" else "users"; guiAddress = "0.0.0.0:8384"; overrideFolders = true; overrideDevices = true; @@ -30,10 +26,7 @@ }; folders = { "Projects" = { - path = - if pkgs.stdenv.isDarwin - then "/Users/${user}/Projects" - else "/home/${user}/Projects"; + path = "${homeDir}/Projects"; devices = ["tahani" "jason"]; }; }; diff --git a/overlays/README.md b/overlays/README.md deleted file mode 100644 index f52985e..0000000 --- a/overlays/README.md +++ /dev/null @@ -1,3 +0,0 @@ -# Overlays - -Files in this directory run automatically as part of each build. diff --git a/overlays/syncthing-darwin.nix b/overlays/syncthing-darwin.nix index b37221f..046d2ab 100644 --- a/overlays/syncthing-darwin.nix +++ b/overlays/syncthing-darwin.nix @@ -1,383 +1,386 @@ final: prev: { - # Create a Darwin-compatible Syncthing service module - darwinModules = prev.darwinModules or {} // { - syncthing = { 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; + 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) == "/"; + isUnixGui = (builtins.substring 0 1 cfg.guiAddress) == "/"; - curlAddressArgs = path: - if isUnixGui - then "--unix-socket ${cfg.guiAddress} http://.${path}" - else "${cfg.guiAddress}${path}"; + 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; + 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); + 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 + 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"; + 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."; }; - dirs = { - new_conf_IDs = map (v: v.id) folders; - GET_IdAttrName = "id"; - override = cfg.overrideFolders; - conf = folders; - baseAddress = curlAddressArgs "/rest/config/folders"; + + key = mkOption { + type = types.nullOr types.str; + default = null; + description = "Path to the key.pem file, which will be copied into Syncthing's configDir."; }; - } [ - (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."; - }; + overrideDevices = mkOption { + type = types.bool; + default = true; + description = "Whether to delete the devices which are not configured via the devices option."; + }; - key = mkOption { - type = types.nullOr types.str; - default = null; - description = "Path to the key.pem file, which will be copied into Syncthing's configDir."; - }; + overrideFolders = mkOption { + type = types.bool; + default = !anyAutoAccept; + description = "Whether to delete the folders which are not configured via the folders option."; + }; - 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."; + 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."; + 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."; + }; }; - 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."; + 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."; + }; }; - 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 = "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"; }; - 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."; + 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"; + }; }; - 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."; - }; - - package = mkPackageOption pkgs "syncthing" { }; + 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 + ''} + ''; }; }; - - 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 +}