449 lines
		
	
	
		
			14 KiB
		
	
	
	
		
			Nix
		
	
	
	
	
	
			
		
		
	
	
			449 lines
		
	
	
		
			14 KiB
		
	
	
	
		
			Nix
		
	
	
	
	
	
| { config, lib, pkgs, ... }:
 | |
| 
 | |
| with lib;
 | |
| 
 | |
| let
 | |
|   cfg = config.services.syncthing;
 | |
|   defaultUser = "syncthing";
 | |
| 
 | |
|   devices = mapAttrsToList (name: device: {
 | |
|     deviceID = device.id;
 | |
|     inherit (device) name addresses introducer;
 | |
|   }) cfg.declarative.devices;
 | |
| 
 | |
|   folders = mapAttrsToList ( _: folder: {
 | |
|     inherit (folder) path id label type;
 | |
|     devices = map (device: { deviceId = cfg.declarative.devices.${device}.id; }) folder.devices;
 | |
|     rescanIntervalS = folder.rescanInterval;
 | |
|     fsWatcherEnabled = folder.watch;
 | |
|     fsWatcherDelayS = folder.watchDelay;
 | |
|     ignorePerms = folder.ignorePerms;
 | |
|   }) (filterAttrs (
 | |
|     _: folder:
 | |
|     folder.enable
 | |
|   ) cfg.declarative.folders);
 | |
| 
 | |
|   # get the api key by parsing the config.xml
 | |
|   getApiKey = pkgs.writers.writeDash "getAPIKey" ''
 | |
|     ${pkgs.libxml2}/bin/xmllint \
 | |
|       --xpath 'string(configuration/gui/apikey)'\
 | |
|       ${cfg.configDir}/config.xml
 | |
|   '';
 | |
| 
 | |
|   updateConfig = pkgs.writers.writeDash "merge-syncthing-config" ''
 | |
|     set -efu
 | |
|     # wait for syncthing port to open
 | |
|     until ${pkgs.curl}/bin/curl -Ss ${cfg.guiAddress} -o /dev/null; do
 | |
|       sleep 1
 | |
|     done
 | |
| 
 | |
|     API_KEY=$(${getApiKey})
 | |
|     OLD_CFG=$(${pkgs.curl}/bin/curl -Ss \
 | |
|       -H "X-API-Key: $API_KEY" \
 | |
|       ${cfg.guiAddress}/rest/system/config)
 | |
| 
 | |
|     # generate the new config by merging with the nixos config options
 | |
|     NEW_CFG=$(echo "$OLD_CFG" | ${pkgs.jq}/bin/jq -s '.[] as $in | $in * {
 | |
|       "devices": (${builtins.toJSON devices}${optionalString (! cfg.declarative.overrideDevices) " + $in.devices"}),
 | |
|       "folders": (${builtins.toJSON folders}${optionalString (! cfg.declarative.overrideFolders) " + $in.folders"})
 | |
|     }')
 | |
| 
 | |
|     # POST the new config to syncthing
 | |
|     echo "$NEW_CFG" | ${pkgs.curl}/bin/curl -Ss \
 | |
|       -H "X-API-Key: $API_KEY" \
 | |
|       ${cfg.guiAddress}/rest/system/config -d @-
 | |
| 
 | |
|     # restart syncthing after sending the new config
 | |
|     ${pkgs.curl}/bin/curl -Ss \
 | |
|       -H "X-API-Key: $API_KEY" \
 | |
|       -X POST \
 | |
|       ${cfg.guiAddress}/rest/system/restart
 | |
|   '';
 | |
| in {
 | |
|   ###### interface
 | |
|   options = {
 | |
|     services.syncthing = {
 | |
| 
 | |
|       enable = mkEnableOption ''
 | |
|         Syncthing - the self-hosted open-source alternative
 | |
|         to Dropbox and Bittorrent Sync. Initial interface will be
 | |
|         available on http://127.0.0.1:8384/.
 | |
|       '';
 | |
| 
 | |
|       declarative = {
 | |
|         cert = mkOption {
 | |
|           type = types.nullOr types.str;
 | |
|           default = null;
 | |
|           description = ''
 | |
|             Path to users cert.pem file, will be copied into the syncthing's
 | |
|             <literal>configDir</literal>
 | |
|           '';
 | |
|         };
 | |
| 
 | |
|         key = mkOption {
 | |
|           type = types.nullOr types.str;
 | |
|           default = null;
 | |
|           description = ''
 | |
|             Path to users key.pem file, will be copied into the syncthing's
 | |
|             <literal>configDir</literal>
 | |
|           '';
 | |
|         };
 | |
| 
 | |
|         overrideDevices = mkOption {
 | |
|           type = types.bool;
 | |
|           default = true;
 | |
|           description = ''
 | |
|             Whether to delete the devices which are not configured via the
 | |
|             <literal>declarative.devices</literal> option.
 | |
|             If set to false, devices added via the webinterface will
 | |
|             persist but will have to be deleted manually.
 | |
|           '';
 | |
|         };
 | |
| 
 | |
|         devices = mkOption {
 | |
|           default = {};
 | |
|           description = ''
 | |
|             Peers/devices which syncthing should communicate with.
 | |
|           '';
 | |
|           example = {
 | |
|             bigbox = {
 | |
|               id = "7CFNTQM-IMTJBHJ-3UWRDIU-ZGQJFR6-VCXZ3NB-XUH3KZO-N52ITXR-LAIYUAU";
 | |
|               addresses = [ "tcp://192.168.0.10:51820" ];
 | |
|             };
 | |
|           };
 | |
|           type = types.attrsOf (types.submodule ({ config, ... }: {
 | |
|             options = {
 | |
| 
 | |
|               name = mkOption {
 | |
|                 type = types.str;
 | |
|                 default = config._module.args.name;
 | |
|                 description = ''
 | |
|                   Name of the device
 | |
|                 '';
 | |
|               };
 | |
| 
 | |
|               addresses = mkOption {
 | |
|                 type = types.listOf types.str;
 | |
|                 default = [];
 | |
|                 description = ''
 | |
|                   The addresses used to connect to the device.
 | |
|                   If this is let empty, dynamic configuration is attempted
 | |
|                 '';
 | |
|               };
 | |
| 
 | |
|               id = mkOption {
 | |
|                 type = types.str;
 | |
|                 description = ''
 | |
|                   The id of the other peer, this is mandatory. It's documented at
 | |
|                   https://docs.syncthing.net/dev/device-ids.html
 | |
|                 '';
 | |
|               };
 | |
| 
 | |
|               introducer = mkOption {
 | |
|                 type = types.bool;
 | |
|                 default = false;
 | |
|                 description = ''
 | |
|                   If the device should act as an introducer and be allowed
 | |
|                   to add folders on this computer.
 | |
|                 '';
 | |
|               };
 | |
| 
 | |
|             };
 | |
|           }));
 | |
|         };
 | |
| 
 | |
|         overrideFolders = mkOption {
 | |
|           type = types.bool;
 | |
|           default = true;
 | |
|           description = ''
 | |
|             Whether to delete the folders which are not configured via the
 | |
|             <literal>declarative.folders</literal> option.
 | |
|             If set to false, folders added via the webinterface will persist
 | |
|             but will have to be deleted manually.
 | |
|           '';
 | |
|         };
 | |
| 
 | |
|         folders = mkOption {
 | |
|           default = {};
 | |
|           description = ''
 | |
|             folders which should be shared by syncthing.
 | |
|           '';
 | |
|           example = {
 | |
|             "/home/user/sync" = {
 | |
|               id = "syncme";
 | |
|               devices = [ "bigbox" ];
 | |
|             };
 | |
|           };
 | |
|           type = types.attrsOf (types.submodule ({ config, ... }: {
 | |
|             options = {
 | |
| 
 | |
|               enable = mkOption {
 | |
|                 type = types.bool;
 | |
|                 default = true;
 | |
|                 description = ''
 | |
|                   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 {
 | |
|                 type = types.str;
 | |
|                 default = config._module.args.name;
 | |
|                 description = ''
 | |
|                   The path to the folder which should be shared.
 | |
|                 '';
 | |
|               };
 | |
| 
 | |
|               id = mkOption {
 | |
|                 type = types.str;
 | |
|                 default = config._module.args.name;
 | |
|                 description = ''
 | |
|                   The id of the folder. Must be the same on all devices.
 | |
|                 '';
 | |
|               };
 | |
| 
 | |
|               label = mkOption {
 | |
|                 type = types.str;
 | |
|                 default = config._module.args.name;
 | |
|                 description = ''
 | |
|                   The label of the folder.
 | |
|                 '';
 | |
|               };
 | |
| 
 | |
|               devices = mkOption {
 | |
|                 type = types.listOf types.str;
 | |
|                 default = [];
 | |
|                 description = ''
 | |
|                   The devices this folder should be shared with. Must be defined
 | |
|                   in the <literal>declarative.devices</literal> attribute.
 | |
|                 '';
 | |
|               };
 | |
| 
 | |
|               rescanInterval = mkOption {
 | |
|                 type = types.int;
 | |
|                 default = 3600;
 | |
|                 description = ''
 | |
|                   How often the folders should be rescaned for changes.
 | |
|                 '';
 | |
|               };
 | |
| 
 | |
|               type = mkOption {
 | |
|                 type = types.enum [ "sendreceive" "sendonly" "receiveonly" ];
 | |
|                 default = "sendreceive";
 | |
|                 description = ''
 | |
|                   Whether to send only changes from this folder, only receive them
 | |
|                   or propagate both.
 | |
|                 '';
 | |
|               };
 | |
| 
 | |
|               watch = mkOption {
 | |
|                 type = types.bool;
 | |
|                 default = true;
 | |
|                 description = ''
 | |
|                   Whether the folder should be watched for changes by inotify.
 | |
|                 '';
 | |
|               };
 | |
| 
 | |
|               watchDelay = mkOption {
 | |
|                 type = types.int;
 | |
|                 default = 10;
 | |
|                 description = ''
 | |
|                   The delay after an inotify event is triggered.
 | |
|                 '';
 | |
|               };
 | |
| 
 | |
|               ignorePerms = mkOption {
 | |
|                 type = types.bool;
 | |
|                 default = true;
 | |
|                 description = ''
 | |
|                   Whether to propagate permission changes.
 | |
|                 '';
 | |
|               };
 | |
| 
 | |
|             };
 | |
|           }));
 | |
|         };
 | |
|       };
 | |
| 
 | |
|       guiAddress = mkOption {
 | |
|         type = types.str;
 | |
|         default = "127.0.0.1:8384";
 | |
|         description = ''
 | |
|           Address to serve the GUI.
 | |
|         '';
 | |
|       };
 | |
| 
 | |
|       systemService = mkOption {
 | |
|         type = types.bool;
 | |
|         default = true;
 | |
|         description = "Auto launch Syncthing as a system service.";
 | |
|       };
 | |
| 
 | |
|       user = mkOption {
 | |
|         type = types.str;
 | |
|         default = defaultUser;
 | |
|         description = ''
 | |
|           Syncthing will be run under this user (user will be created if it doesn't exist.
 | |
|           This can be your user name).
 | |
|         '';
 | |
|       };
 | |
| 
 | |
|       group = mkOption {
 | |
|         type = types.str;
 | |
|         default = "nogroup";
 | |
|         description = ''
 | |
|           Syncthing will be run under this group (group will not be created if it doesn't exist.
 | |
|           This can be your user name).
 | |
|         '';
 | |
|       };
 | |
| 
 | |
|       all_proxy = mkOption {
 | |
|         type = with types; nullOr str;
 | |
|         default = null;
 | |
|         example = "socks5://address.com:1234";
 | |
|         description = ''
 | |
|           Overwrites all_proxy environment variable for the syncthing process to
 | |
|           the given value. This is normaly used to let relay client connect
 | |
|           through SOCKS5 proxy server.
 | |
|         '';
 | |
|       };
 | |
| 
 | |
|       dataDir = mkOption {
 | |
|         type = types.path;
 | |
|         default = "/var/lib/syncthing";
 | |
|         description = ''
 | |
|           Path where synced directories will exist.
 | |
|         '';
 | |
|       };
 | |
| 
 | |
|       configDir = mkOption {
 | |
|         type = types.path;
 | |
|         description = ''
 | |
|           Path where the settings and keys will exist.
 | |
|         '';
 | |
|         default =
 | |
|           let
 | |
|             nixos = config.system.stateVersion;
 | |
|             cond  = versionAtLeast nixos "19.03";
 | |
|           in cfg.dataDir + (optionalString cond "/.config/syncthing");
 | |
|       };
 | |
| 
 | |
|       openDefaultPorts = mkOption {
 | |
|         type = types.bool;
 | |
|         default = false;
 | |
|         example = literalExample "true";
 | |
|         description = ''
 | |
|           Open the default ports in the firewall:
 | |
|             - TCP 22000 for transfers
 | |
|             - UDP 21027 for discovery
 | |
|           If multiple users are running syncthing on this machine, you will need to manually open a set of ports for each instance and leave this disabled.
 | |
|           Alternatively, if are running only a single instance on this machine using the default ports, enable this.
 | |
|         '';
 | |
|       };
 | |
| 
 | |
|       package = mkOption {
 | |
|         type = types.package;
 | |
|         default = pkgs.syncthing;
 | |
|         defaultText = "pkgs.syncthing";
 | |
|         example = literalExample "pkgs.syncthing";
 | |
|         description = ''
 | |
|           Syncthing package to use.
 | |
|         '';
 | |
|       };
 | |
|     };
 | |
|   };
 | |
| 
 | |
|   imports = [
 | |
|     (mkRemovedOptionModule ["services" "syncthing" "useInotify"] ''
 | |
|       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 webinterface.
 | |
|     '')
 | |
|   ];
 | |
| 
 | |
|   ###### implementation
 | |
| 
 | |
|   config = mkIf cfg.enable {
 | |
| 
 | |
|     networking.firewall = mkIf cfg.openDefaultPorts {
 | |
|       allowedTCPPorts = [ 22000 ];
 | |
|       allowedUDPPorts = [ 21027 ];
 | |
|     };
 | |
| 
 | |
|     systemd.packages = [ pkgs.syncthing ];
 | |
| 
 | |
|     users = mkIf (cfg.systemService && cfg.user == defaultUser) {
 | |
|       users."${defaultUser}" =
 | |
|         { group = cfg.group;
 | |
|           home  = cfg.dataDir;
 | |
|           createHome = true;
 | |
|           uid = config.ids.uids.syncthing;
 | |
|           description = "Syncthing daemon user";
 | |
|         };
 | |
| 
 | |
|       groups."${defaultUser}".gid =
 | |
|         config.ids.gids.syncthing;
 | |
|     };
 | |
| 
 | |
|     systemd.services = {
 | |
|       syncthing = mkIf cfg.systemService {
 | |
|         description = "Syncthing service";
 | |
|         after = [ "network.target" ];
 | |
|         environment = {
 | |
|           STNORESTART = "yes";
 | |
|           STNOUPGRADE = "yes";
 | |
|           inherit (cfg) all_proxy;
 | |
|         } // config.networking.proxy.envVars;
 | |
|         wantedBy = [ "multi-user.target" ];
 | |
|         serviceConfig = {
 | |
|           Restart = "on-failure";
 | |
|           SuccessExitStatus = "2 3 4";
 | |
|           RestartForceExitStatus="3 4";
 | |
|           User = cfg.user;
 | |
|           Group = cfg.group;
 | |
|           ExecStartPre = mkIf (cfg.declarative.cert != null || cfg.declarative.key != null)
 | |
|             "+${pkgs.writers.writeBash "syncthing-copy-keys" ''
 | |
|               mkdir -p ${cfg.configDir}
 | |
|               chown ${cfg.user}:${cfg.group} ${cfg.configDir}
 | |
|               chmod 700 ${cfg.configDir}
 | |
|               ${optionalString (cfg.declarative.cert != null) ''
 | |
|                 cp ${toString cfg.declarative.cert} ${cfg.configDir}/cert.pem
 | |
|                 chown ${cfg.user}:${cfg.group} ${cfg.configDir}/cert.pem
 | |
|                 chmod 400 ${cfg.configDir}/cert.pem
 | |
|               ''}
 | |
|               ${optionalString (cfg.declarative.key != null) ''
 | |
|                 cp ${toString cfg.declarative.key} ${cfg.configDir}/key.pem
 | |
|                 chown ${cfg.user}:${cfg.group} ${cfg.configDir}/key.pem
 | |
|                 chmod 400 ${cfg.configDir}/key.pem
 | |
|               ''}
 | |
|             ''}"
 | |
|           ;
 | |
|           ExecStart = ''
 | |
|             ${cfg.package}/bin/syncthing \
 | |
|               -no-browser \
 | |
|               -gui-address=${cfg.guiAddress} \
 | |
|               -home=${cfg.configDir}
 | |
|           '';
 | |
|         };
 | |
|       };
 | |
|       syncthing-init = mkIf (
 | |
|         cfg.declarative.devices != {} || cfg.declarative.folders != {}
 | |
|       ) {
 | |
|         after = [ "syncthing.service" ];
 | |
|         wantedBy = [ "multi-user.target" ];
 | |
| 
 | |
|         serviceConfig = {
 | |
|           User = cfg.user;
 | |
|           RemainAfterExit = true;
 | |
|           Type = "oneshot";
 | |
|           ExecStart = updateConfig;
 | |
|         };
 | |
|       };
 | |
| 
 | |
|       syncthing-resume = {
 | |
|         wantedBy = [ "suspend.target" ];
 | |
|       };
 | |
|     };
 | |
|   };
 | |
| }
 | 
