diff --git a/nixos/modules/services/networking/syncthing.nix b/nixos/modules/services/networking/syncthing.nix index 69b45eb02d1d..56da95dca94d 100644 --- a/nixos/modules/services/networking/syncthing.nix +++ b/nixos/modules/services/networking/syncthing.nix @@ -7,25 +7,27 @@ let opt = options.services.syncthing; defaultUser = "syncthing"; defaultGroup = defaultUser; + settingsFormat = pkgs.formats.json { }; + cleanedConfig = converge (filterAttrsRecursive (_: v: v != null && v != {})) cfg.settings; - devices = mapAttrsToList (name: device: { + devices = mapAttrsToList (_: device: device // { deviceID = device.id; - inherit (device) name addresses introducer autoAcceptFolders; - }) cfg.devices; + }) cfg.settings.devices; - folders = mapAttrsToList ( _: folder: { - inherit (folder) path id label type; - devices = map (device: { deviceId = cfg.devices.${device}.id; }) folder.devices; - rescanIntervalS = folder.rescanInterval; - fsWatcherEnabled = folder.watch; - fsWatcherDelayS = folder.watchDelay; - ignorePerms = folder.ignorePerms; - ignoreDelete = folder.ignoreDelete; - versioning = folder.versioning; - }) (filterAttrs ( - _: folder: + folders = mapAttrsToList (_: folder: folder // + throwIf (folder?rescanInterval || folder?watch || folder?watchDelay) '' + The options services.syncthing.settings.folders..{rescanInterval,watch,watchDelay} + were removed. Please use, respectively, {rescanIntervalS,fsWatcherEnabled,fsWatcherDelayS} instead. + '' { + devices = map (device: + if builtins.isString device then + { deviceId = cfg.settings.devices.${device}.id; } + else + device + ) folder.devices; + }) (filterAttrs (_: folder: folder.enable - ) cfg.folders); + ) cfg.settings.folders); updateConfig = pkgs.writers.writeDash "merge-syncthing-config" '' set -efu @@ -54,10 +56,10 @@ let old_cfg=$(curl ${cfg.guiAddress}/rest/config) # generate the new config by merging with the NixOS config options - new_cfg=$(printf '%s\n' "$old_cfg" | ${pkgs.jq}/bin/jq -c '. * { - "devices": ('${escapeShellArg (builtins.toJSON devices)}'${optionalString (cfg.devices == {} || ! cfg.overrideDevices) " + .devices"}), - "folders": ('${escapeShellArg (builtins.toJSON folders)}'${optionalString (cfg.folders == {} || ! cfg.overrideFolders) " + .folders"}) - } * '${escapeShellArg (builtins.toJSON cfg.extraOptions)}) + new_cfg=$(printf '%s\n' "$old_cfg" | ${pkgs.jq}/bin/jq -c ${escapeShellArg ''. * ${builtins.toJSON cleanedConfig} * { + "devices": ('${escapeShellArg (builtins.toJSON devices)}'${optionalString (cfg.settings.devices == {} || ! cfg.overrideDevices) " + .devices"}), + "folders": ('${escapeShellArg (builtins.toJSON folders)}'${optionalString (cfg.settings.folders == {} || ! cfg.overrideFolders) " + .folders"}) + }''}) # send the new config curl -X PUT -d "$new_cfg" ${cfg.guiAddress}/rest/config @@ -99,287 +101,282 @@ in { default = true; description = mdDoc '' Whether to delete the devices which are not configured via the - [devices](#opt-services.syncthing.devices) option. + [devices](#opt-services.syncthing.settings.devices) option. If set to `false`, devices added via the web interface will persist and will have to be deleted manually. ''; }; - devices = mkOption { - default = {}; - description = mdDoc '' - Peers/devices which Syncthing should communicate with. - - Note that you can still add devices manually, but those changes - will be reverted on restart if [overrideDevices](#opt-services.syncthing.overrideDevices) - is enabled. - ''; - example = { - bigbox = { - id = "7CFNTQM-IMTJBHJ-3UWRDIU-ZGQJFR6-VCXZ3NB-XUH3KZO-N52ITXR-LAIYUAU"; - addresses = [ "tcp://192.168.0.10:51820" ]; - }; - }; - type = types.attrsOf (types.submodule ({ name, ... }: { - options = { - - name = mkOption { - type = types.str; - default = name; - description = lib.mdDoc '' - The name of the device. - ''; - }; - - addresses = mkOption { - type = types.listOf types.str; - default = []; - description = lib.mdDoc '' - The addresses used to connect to the device. - If this is left empty, dynamic configuration is attempted. - ''; - }; - - id = mkOption { - type = types.str; - description = mdDoc '' - The device ID. See . - ''; - }; - - introducer = mkOption { - type = types.bool; - default = false; - description = mdDoc '' - Whether the device should act as an introducer and be allowed - to add folders on this computer. - See . - ''; - }; - - autoAcceptFolders = mkOption { - type = types.bool; - default = false; - description = mdDoc '' - Automatically create or share folders that this device advertises at the default path. - See . - ''; - }; - - }; - })); - }; - overrideFolders = mkOption { type = types.bool; default = true; description = mdDoc '' Whether to delete the folders which are not configured via the - [folders](#opt-services.syncthing.folders) option. + [folders](#opt-services.syncthing.settings.folders) option. If set to `false`, folders added via the web interface will persist and will have to be deleted manually. ''; }; - folders = mkOption { - default = {}; - description = mdDoc '' - Folders which should be shared by Syncthing. - - Note that you can still add folders manually, but those changes - will be reverted on restart if [overrideFolders](#opt-services.syncthing.overrideFolders) - is enabled. - ''; - example = literalExpression '' - { - "/home/user/sync" = { - id = "syncme"; - devices = [ "bigbox" ]; - }; - } - ''; - type = types.attrsOf (types.submodule ({ name, ... }: { + settings = mkOption { + type = types.submodule { + freeformType = settingsFormat.type; options = { - - enable = mkOption { - type = types.bool; - default = true; - description = lib.mdDoc '' - Whether to share this folder. - This option is useful when you want to define all folders - in one place, but not every machine should share all folders. - ''; - }; - - path = mkOption { - # TODO for release 23.05: allow relative paths again and set - # working directory to cfg.dataDir - type = types.str // { - check = x: types.str.check x && (substring 0 1 x == "/" || substring 0 2 x == "~/"); - description = types.str.description + " starting with / or ~/"; - }; - default = name; - description = lib.mdDoc '' - The path to the folder which should be shared. - Only absolute paths (starting with `/`) and paths relative to - the [user](#opt-services.syncthing.user)'s home directory - (starting with `~/`) are allowed. - ''; - }; - - id = mkOption { - type = types.str; - default = name; - description = lib.mdDoc '' - The ID of the folder. Must be the same on all devices. - ''; - }; - - label = mkOption { - type = types.str; - default = name; - description = lib.mdDoc '' - The label of the folder. - ''; - }; - - devices = mkOption { - type = types.listOf types.str; - default = []; + # global options + options = mkOption { + default = {}; description = mdDoc '' - The devices this folder should be shared with. Each device must - be defined in the [devices](#opt-services.syncthing.devices) option. + The options element contains all other global configuration options ''; - }; - - versioning = mkOption { - default = null; - description = mdDoc '' - How to keep changed/deleted files with Syncthing. - There are 4 different types of versioning with different parameters. - See . - ''; - example = literalExpression '' - [ - { - versioning = { - type = "simple"; - params.keep = "10"; - }; - } - { - versioning = { - type = "trashcan"; - params.cleanoutDays = "1000"; - }; - } - { - versioning = { - type = "staggered"; - fsPath = "/syncthing/backup"; - params = { - cleanInterval = "3600"; - maxAge = "31536000"; - }; - }; - } - { - versioning = { - type = "external"; - params.versionsPath = pkgs.writers.writeBash "backup" ''' - folderpath="$1" - filepath="$2" - rm -rf "$folderpath/$filepath" - '''; - }; - } - ] - ''; - type = with types; nullOr (submodule { + type = types.submodule ({ name, ... }: { + freeformType = settingsFormat.type; options = { - type = mkOption { - type = enum [ "external" "simple" "staggered" "trashcan" ]; - description = mdDoc '' - The type of versioning. - See . + localAnnounceEnabled = mkOption { + type = types.nullOr types.bool; + default = null; + description = lib.mdDoc '' + Whether to send announcements to the local LAN, also use such announcements to find other devices. ''; }; - fsPath = mkOption { - default = ""; - type = either str path; - description = mdDoc '' - Path to the versioning folder. - See . + + localAnnouncePort = mkOption { + type = types.nullOr types.int; + default = null; + description = lib.mdDoc '' + The port on which to listen and send IPv4 broadcast announcements to. ''; }; - params = mkOption { - type = attrsOf (either str path); - description = mdDoc '' - The parameters for versioning. Structure depends on - [versioning.type](#opt-services.syncthing.folders._name_.versioning.type). - See . + + relaysEnabled = mkOption { + type = types.nullOr types.bool; + default = null; + description = lib.mdDoc '' + 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 = lib.mdDoc '' + Whether the user has accepted to submit anonymous usage data. + The default, 0, mean the user has not made a choice, and Syncthing will ask at some point in the future. + "-1" means no, a number above zero means that that version of usage reporting has been accepted. + ''; + }; + + limitBandwidthInLan = mkOption { + type = types.nullOr types.bool; + default = null; + description = lib.mdDoc '' + Whether to apply bandwidth limits to devices in the same broadcast domain as the local device. + ''; + }; + + maxFolderConcurrency = mkOption { + type = types.nullOr types.int; + default = null; + description = lib.mdDoc '' + This option controls how many folders may concurrently be in I/O-intensive operations such as syncing or scanning. + The mechanism is described in detail in a [separate chapter](https://docs.syncthing.net/advanced/option-max-concurrency.html). ''; }; }; }); }; - rescanInterval = mkOption { - type = types.int; - default = 3600; - description = lib.mdDoc '' - How often the folder should be rescanned for changes. - ''; - }; - - type = mkOption { - type = types.enum [ "sendreceive" "sendonly" "receiveonly" "receiveencrypted" ]; - default = "sendreceive"; - description = lib.mdDoc '' - Whether to only send changes for this folder, only receive them - or both. `receiveencrypted` can be used for untrusted devices. See - for reference. - ''; - }; - - watch = mkOption { - type = types.bool; - default = true; - description = lib.mdDoc '' - Whether the folder should be watched for changes by inotify. - ''; - }; - - watchDelay = mkOption { - type = types.int; - default = 10; - description = lib.mdDoc '' - The delay after an inotify event is triggered. - ''; - }; - - ignorePerms = mkOption { - type = types.bool; - default = true; - description = lib.mdDoc '' - Whether to ignore permission changes. - ''; - }; - - ignoreDelete = mkOption { - type = types.bool; - default = false; + # device settings + devices = mkOption { + default = {}; description = mdDoc '' - Whether to skip deleting files that are deleted by peers. - See . - ''; - }; - }; - })); - }; + Peers/devices which Syncthing should communicate with. - extraOptions = mkOption { - type = types.addCheck (pkgs.formats.json {}).type isAttrs; + Note that you can still add devices manually, but those changes + will be reverted on restart if [overrideDevices](#opt-services.syncthing.overrideDevices) + is enabled. + ''; + example = { + bigbox = { + id = "7CFNTQM-IMTJBHJ-3UWRDIU-ZGQJFR6-VCXZ3NB-XUH3KZO-N52ITXR-LAIYUAU"; + addresses = [ "tcp://192.168.0.10:51820" ]; + }; + }; + type = types.attrsOf (types.submodule ({ name, ... }: { + freeformType = settingsFormat.type; + options = { + + name = mkOption { + type = types.str; + default = name; + description = lib.mdDoc '' + The name of the device. + ''; + }; + + id = mkOption { + type = types.str; + description = mdDoc '' + The device ID. See . + ''; + }; + + autoAcceptFolders = mkOption { + type = types.bool; + default = false; + description = mdDoc '' + Automatically create or share folders that this device advertises at the default path. + See . + ''; + }; + + }; + })); + }; + + # folder settings + folders = mkOption { + default = {}; + description = mdDoc '' + Folders which should be shared by Syncthing. + + Note that you can still add folders manually, but those changes + will be reverted on restart if [overrideFolders](#opt-services.syncthing.overrideFolders) + is enabled. + ''; + example = literalExpression '' + { + "/home/user/sync" = { + id = "syncme"; + devices = [ "bigbox" ]; + }; + } + ''; + type = types.attrsOf (types.submodule ({ name, ... }: { + freeformType = settingsFormat.type; + options = { + + enable = mkOption { + type = types.bool; + default = true; + description = lib.mdDoc '' + Whether to share this folder. + This option is useful when you want to define all folders + in one place, but not every machine should share all folders. + ''; + }; + + path = mkOption { + # TODO for release 23.05: allow relative paths again and set + # working directory to cfg.dataDir + type = types.str // { + check = x: types.str.check x && (substring 0 1 x == "/" || substring 0 2 x == "~/"); + description = types.str.description + " starting with / or ~/"; + }; + default = name; + description = lib.mdDoc '' + The path to the folder which should be shared. + Only absolute paths (starting with `/`) and paths relative to + the [user](#opt-services.syncthing.user)'s home directory + (starting with `~/`) are allowed. + ''; + }; + + id = mkOption { + type = types.str; + default = name; + description = lib.mdDoc '' + The ID of the folder. Must be the same on all devices. + ''; + }; + + label = mkOption { + type = types.str; + default = name; + description = lib.mdDoc '' + The label of the folder. + ''; + }; + + devices = mkOption { + type = types.listOf types.str; + default = []; + description = mdDoc '' + The devices this folder should be shared with. Each device must + be defined in the [devices](#opt-services.syncthing.settings.devices) option. + ''; + }; + + versioning = mkOption { + default = null; + description = mdDoc '' + How to keep changed/deleted files with Syncthing. + There are 4 different types of versioning with different parameters. + See . + ''; + example = literalExpression '' + [ + { + versioning = { + type = "simple"; + params.keep = "10"; + }; + } + { + versioning = { + type = "trashcan"; + params.cleanoutDays = "1000"; + }; + } + { + versioning = { + type = "staggered"; + fsPath = "/syncthing/backup"; + params = { + cleanInterval = "3600"; + maxAge = "31536000"; + }; + }; + } + { + versioning = { + type = "external"; + params.versionsPath = pkgs.writers.writeBash "backup" ''' + folderpath="$1" + filepath="$2" + rm -rf "$folderpath/$filepath" + '''; + }; + } + ] + ''; + type = with types; nullOr (submodule { + freeformType = settingsFormat.type; + options = { + type = mkOption { + type = enum [ "external" "simple" "staggered" "trashcan" ]; + description = mdDoc '' + The type of versioning. + See . + ''; + }; + }; + }); + }; + + copyOwnershipFromParent = mkOption { + type = types.bool; + default = false; + description = mdDoc '' + On Unix systems, tries to copy file/folder ownership from the parent directory (the directory it’s located in). + Requires running Syncthing as a privileged user, or granting it additional capabilities (e.g. CAP_CHOWN on Linux). + ''; + }; + }; + })); + }; + + }; + }; default = {}; description = mdDoc '' Extra configuration options for Syncthing. @@ -530,6 +527,10 @@ in { This option was removed because Syncthing now has the inotify functionality included under the name "fswatcher". It can be enabled on a per-folder basis through the web interface. '') + (mkRenamedOptionModule [ "services" "syncthing" "extraOptions" ] [ "services" "syncthing" "settings" ]) + (mkRenamedOptionModule [ "services" "syncthing" "folders" ] [ "services" "syncthing" "settings" "folders" ]) + (mkRenamedOptionModule [ "services" "syncthing" "devices" ] [ "services" "syncthing" "settings" "devices" ]) + (mkRenamedOptionModule [ "services" "syncthing" "options" ] [ "services" "syncthing" "settings" "options" ]) ] ++ map (o: mkRenamedOptionModule [ "services" "syncthing" "declarative" o ] [ "services" "syncthing" o ] ) [ "cert" "key" "devices" "folders" "overrideDevices" "overrideFolders" "extraOptions"]; @@ -615,9 +616,7 @@ in { ]; }; }; - syncthing-init = mkIf ( - cfg.devices != {} || cfg.folders != {} || cfg.extraOptions != {} - ) { + syncthing-init = mkIf (cleanedConfig != {}) { description = "Syncthing configuration updater"; requisite = [ "syncthing.service" ]; after = [ "syncthing.service" ]; diff --git a/nixos/tests/all-tests.nix b/nixos/tests/all-tests.nix index a64f7c5ccbcf..80cf5cd3771b 100644 --- a/nixos/tests/all-tests.nix +++ b/nixos/tests/all-tests.nix @@ -728,6 +728,7 @@ in { switchTest = handleTest ./switch-test.nix {}; sympa = handleTest ./sympa.nix {}; syncthing = handleTest ./syncthing.nix {}; + syncthing-no-settings = handleTest ./syncthing-no-settings.nix {}; syncthing-init = handleTest ./syncthing-init.nix {}; syncthing-relay = handleTest ./syncthing-relay.nix {}; systemd = handleTest ./systemd.nix {}; diff --git a/nixos/tests/syncthing-init.nix b/nixos/tests/syncthing-init.nix index 5102c0127832..195c157ffb6e 100644 --- a/nixos/tests/syncthing-init.nix +++ b/nixos/tests/syncthing-init.nix @@ -10,14 +10,14 @@ in { nodes.machine = { services.syncthing = { enable = true; - devices.${testName} = { + settings.devices.testDevice = { id = testId; }; - folders.testFolder = { + settings.folders.testFolder = { path = "/tmp/test"; - devices = [ testName ]; + devices = [ "testDevice" ]; }; - extraOptions.gui.user = "guiUser"; + settings.gui.user = "guiUser"; }; }; diff --git a/nixos/tests/syncthing-no-settings.nix b/nixos/tests/syncthing-no-settings.nix new file mode 100644 index 000000000000..fee122b5e35c --- /dev/null +++ b/nixos/tests/syncthing-no-settings.nix @@ -0,0 +1,18 @@ +import ./make-test-python.nix ({ lib, pkgs, ... }: { + name = "syncthing"; + meta.maintainers = with pkgs.lib.maintainers; [ chkno ]; + + nodes = { + a = { + environment.systemPackages = with pkgs; [ curl libxml2 syncthing ]; + services.syncthing = { + enable = true; + }; + }; + }; + # Test that indeed a syncthing-init.service systemd service is not created. + # + testScript = /* python */ '' + a.succeed("systemctl list-unit-files | awk '$1 == \"syncthing-init.service\" {exit 1;}'") + ''; +})