# Syncthing Module # # Declarative Syncthing configuration for NixOS. # This wraps the built-in syncthing module with sensible defaults. # # Usage: # services.syncthing-managed.enable = true; # services.syncthing-managed.user = "youruser"; # services.syncthing-managed.devices.laptop.id = "DEVICE-ID-HERE"; # services.syncthing-managed.folders.documents.path = "~/Documents"; { config, lib, pkgs, ... }: with lib; let cfg = config.services.syncthing-managed; in { options.services.syncthing-managed = { enable = mkEnableOption "managed Syncthing file synchronization"; user = mkOption { type = types.str; description = "User to run Syncthing as."; example = "alice"; }; group = mkOption { type = types.str; default = "users"; description = "Group to run Syncthing as."; }; dataDir = mkOption { type = types.str; default = "/home/${cfg.user}"; defaultText = literalExpression ''"/home/''${cfg.user}"''; description = "Default directory for Syncthing data."; }; configDir = mkOption { type = types.str; default = "/home/${cfg.user}/.config/syncthing"; defaultText = literalExpression ''"/home/''${cfg.user}/.config/syncthing"''; description = "Directory for Syncthing configuration."; }; guiAddress = mkOption { type = types.str; default = "127.0.0.1:8384"; description = "Address for the Syncthing web GUI."; }; openFirewall = mkOption { type = types.bool; default = true; description = "Whether to open the firewall for Syncthing."; }; devices = mkOption { type = types.attrsOf (types.submodule { options = { id = mkOption { type = types.str; description = "Device ID (from syncthing CLI or GUI)."; example = "XXXXXXX-XXXXXXX-XXXXXXX-XXXXXXX-XXXXXXX-XXXXXXX-XXXXXXX-XXXXXXX"; }; name = mkOption { type = types.nullOr types.str; default = null; description = "Friendly name for the device."; }; addresses = mkOption { type = types.listOf types.str; default = [ "dynamic" ]; description = "Addresses to connect to this device."; }; autoAcceptFolders = mkOption { type = types.bool; default = false; description = "Automatically accept shared folders from this device."; }; }; }); default = {}; description = "Syncthing devices to connect to."; example = literalExpression '' { laptop = { id = "XXXXXXX-..."; name = "My Laptop"; }; server = { id = "YYYYYYY-..."; addresses = [ "tcp://server.local:22000" ]; }; } ''; }; folders = mkOption { type = types.attrsOf (types.submodule ({ name, ... }: { options = { path = mkOption { type = types.str; description = "Local path to sync."; example = "~/Documents"; }; devices = mkOption { type = types.listOf types.str; default = attrNames cfg.devices; defaultText = literalExpression "attrNames cfg.devices"; description = "Devices to share this folder with."; }; id = mkOption { type = types.str; default = name; description = "Unique folder ID."; }; type = mkOption { type = types.enum [ "sendreceive" "sendonly" "receiveonly" ]; default = "sendreceive"; description = '' Folder type: - sendreceive: Full two-way sync (default) - sendonly: Only send changes, ignore remote changes - receiveonly: Only receive changes, don't send local changes ''; }; versioning = mkOption { type = types.nullOr (types.submodule { options = { type = mkOption { type = types.enum [ "simple" "staggered" "trashcan" "external" ]; default = "simple"; description = "Versioning type."; }; params = mkOption { type = types.attrsOf types.str; default = { keep = "5"; }; description = "Versioning parameters."; }; }; }); default = null; description = "Versioning configuration for this folder."; example = literalExpression '' { type = "staggered"; params = { cleanInterval = "3600"; maxAge = "31536000"; }; } ''; }; ignorePerms = mkOption { type = types.bool; default = false; description = "Ignore permission changes."; }; rescanInterval = mkOption { type = types.int; default = 3600; description = "How often to rescan the folder (seconds). 0 to disable."; }; fsWatcherEnabled = mkOption { type = types.bool; default = true; description = "Use filesystem watcher for real-time sync."; }; }; })); default = {}; description = "Folders to synchronize."; example = literalExpression '' { documents = { path = "~/Documents"; devices = [ "laptop" "desktop" ]; versioning = { type = "simple"; params.keep = "5"; }; }; music = { path = "~/Music"; type = "receiveonly"; # Don't upload changes }; } ''; }; extraOptions = mkOption { type = types.attrs; default = {}; description = "Extra options to pass to services.syncthing.settings."; }; }; config = mkIf cfg.enable { services.syncthing = { enable = true; user = cfg.user; group = cfg.group; dataDir = cfg.dataDir; configDir = cfg.configDir; guiAddress = cfg.guiAddress; overrideDevices = true; # Declarative device management overrideFolders = true; # Declarative folder management settings = { devices = mapAttrs (name: device: { inherit (device) id addresses autoAcceptFolders; name = if device.name != null then device.name else name; }) cfg.devices; folders = mapAttrs (name: folder: { inherit (folder) path id type ignorePerms rescanInterval fsWatcherEnabled; devices = folder.devices; versioning = if folder.versioning != null then folder.versioning else {}; }) cfg.folders; options = { urAccepted = -1; # Disable usage reporting relaysEnabled = true; globalAnnounceEnabled = true; localAnnounceEnabled = true; } // cfg.extraOptions; }; }; # Open firewall if requested networking.firewall = mkIf cfg.openFirewall { allowedTCPPorts = [ 22000 ]; # Syncthing protocol allowedUDPPorts = [ 22000 21027 ]; # Syncthing + discovery }; # Convenience alias environment.systemPackages = [ (pkgs.writeShellScriptBin "st" '' # Syncthing shortcut case "$1" in status) ${pkgs.syncthing}/bin/syncthing cli show system ;; scan) ${pkgs.syncthing}/bin/syncthing cli scan --all ;; errors) ${pkgs.syncthing}/bin/syncthing cli errors ;; *) echo "Usage: st {status|scan|errors}" ;; esac '') ]; }; }