We were already creating a group for the user under which to run syncthing but we were defaulting to running as `nogroup`. Additionally, use `install` instead of multiple calls to mkdir/cp/chown.
		
			
				
	
	
		
			443 lines
		
	
	
		
			14 KiB
		
	
	
	
		
			Nix
		
	
	
	
	
	
			
		
		
	
	
			443 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 = defaultUser;
 | 
						|
        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" ''
 | 
						|
              install -dm700 -o ${cfg.user} -g ${cfg.group} ${cfg.configDir}
 | 
						|
              ${optionalString (cfg.declarative.cert != null) ''
 | 
						|
                install -Dm400 -o ${cfg.user} -g ${cfg.group} ${toString cfg.declarative.cert} ${cfg.configDir}/cert.pem
 | 
						|
              ''}
 | 
						|
              ${optionalString (cfg.declarative.key != null) ''
 | 
						|
                install -Dm400 -o ${cfg.user} -g ${cfg.group} ${toString cfg.declarative.key} ${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" ];
 | 
						|
      };
 | 
						|
    };
 | 
						|
  };
 | 
						|
}
 |