diff --git a/nixos/doc/manual/release-notes/rl-2405.section.md b/nixos/doc/manual/release-notes/rl-2405.section.md index 19ff6f4485cd..2cde7e7ca66b 100644 --- a/nixos/doc/manual/release-notes/rl-2405.section.md +++ b/nixos/doc/manual/release-notes/rl-2405.section.md @@ -446,6 +446,12 @@ The pre-existing [services.ankisyncd](#opt-services.ankisyncd.enable) has been m - The module `services.github-runner` has been removed. To configure a single GitHub Actions Runner refer to `services.github-runners.*`. Note that this will trigger a new runner registration. +- The `services.slskd` has been refactored to include more configuation options in + the freeform `services.slskd.settings` option, and some defaults (including listen ports) + have been changed to match the upstream defaults. Additionally, disk logging is now + disabled by default, and the log rotation timer has been removed. + The nginx virtualhost option is now of the `vhost-options` type. + - The `btrbk` module now automatically selects and provides required compression program depending on the configured `stream_compress` option. Since this replaces the need for the `extraPackages` option, this option will be diff --git a/nixos/modules/services/web-apps/slskd.nix b/nixos/modules/services/web-apps/slskd.nix index 580f66ec3ac9..15a5fd1177ad 100644 --- a/nixos/modules/services/web-apps/slskd.nix +++ b/nixos/modules/services/web-apps/slskd.nix @@ -2,122 +2,250 @@ let settingsFormat = pkgs.formats.yaml {}; + defaultUser = "slskd"; in { options.services.slskd = with lib; with types; { enable = mkEnableOption "enable slskd"; - rotateLogs = mkEnableOption "enable an unit and timer that will rotate logs in /var/slskd/logs"; + package = mkPackageOptionMD pkgs "slskd" { }; - package = mkPackageOption pkgs "slskd" { }; + user = mkOption { + type = types.str; + default = defaultUser; + description = "User account under which slskd runs."; + }; + + group = mkOption { + type = types.str; + default = defaultUser; + description = "Group under which slskd runs."; + }; + + domain = mkOption { + type = types.nullOr types.str; + description = '' + If non-null, enables an nginx reverse proxy virtual host at this FQDN, + at the path configurated with `services.slskd.web.url_base`. + ''; + example = "slskd.example.com"; + }; nginx = mkOption { - description = lib.mdDoc "options for nginx"; - example = { - enable = true; - domain = "example.com"; - contextPath = "/slskd"; - }; - type = submodule ({name, config, ...}: { - options = { - enable = mkEnableOption "enable nginx as a reverse proxy"; - - domainName = mkOption { - type = str; - description = "Domain you want to use"; - }; - contextPath = mkOption { - type = types.path; - default = "/"; - description = lib.mdDoc '' - The context path, i.e., the last part of the slskd - URL. Typically '/' or '/slskd'. Default '/' - ''; - }; - }; - }); + type = types.submodule (import ../web-servers/nginx/vhost-options.nix { inherit config lib; }); + default = {}; + example = lib.literalExpression '' + { + enableACME = true; + forceHttps = true; + } + ''; + description = '' + This option customizes the nginx virtual host set up for slskd. + ''; }; environmentFile = mkOption { type = path; description = '' - Path to a file containing secrets. - It must at least contain the variable `SLSKD_SLSK_PASSWORD` + Path to the environment file sourced on startup. + It must at least contain the variables `SLSKD_SLSK_USERNAME` and `SLSKD_SLSK_PASSWORD`. + Web interface credentials should also be set here in `SLSKD_USERNAME` and `SLSKD_PASSWORD`. + Other, optional credentials like SOCKS5 with `SLSKD_SLSK_PROXY_USERNAME` and `SLSKD_SLSK_PROXY_PASSWORD` + should all reside here instead of in the world-readable nix store. + Variables are documented at https://github.com/slskd/slskd/blob/master/docs/config.md ''; }; openFirewall = mkOption { type = bool; - description = '' - Whether to open the firewall for services.slskd.settings.listen_port"; - ''; + description = "Whether to open the firewall for the soulseek network listen port (not the web interface port)."; default = false; }; settings = mkOption { - description = lib.mdDoc '' - Configuration for slskd, see - [available options](https://github.com/slskd/slskd/blob/master/docs/config.md) - `APP_DIR` is set to /var/lib/slskd, where default download & incomplete directories, - log and databases will be created. + description = '' + Application configuration for slskd. See + [documentation](https://github.com/slskd/slskd/blob/master/docs/config.md). ''; default = {}; type = submodule { freeformType = settingsFormat.type; options = { + remote_file_management = mkEnableOption "modification of share contents through the web ui"; - soulseek = { - username = mkOption { - type = str; - description = "Username on the Soulseek Network"; + flags = { + force_share_scan = mkOption { + type = bool; + description = "Force a rescan of shares on every startup."; }; - listen_port = mkOption { - type = port; - description = "Port to use for communication on the Soulseek Network"; - default = 50000; - }; - }; - - web = { - port = mkOption { - type = port; - default = 5001; - description = "The HTTP listen port"; - }; - url_base = mkOption { - type = path; - default = config.services.slskd.nginx.contextPath; - defaultText = "config.services.slskd.nginx.contextPath"; - description = lib.mdDoc '' - The context path, i.e., the last part of the slskd URL - ''; - }; - }; - - shares = { - directories = mkOption { - type = listOf str; - description = lib.mdDoc '' - Paths to your shared directories. See - [documentation](https://github.com/slskd/slskd/blob/master/docs/config.md#directories) - for advanced usage - ''; + no_version_check = mkOption { + type = bool; + default = true; + visible = false; + description = "Don't perform a version check on startup."; }; }; directories = { incomplete = mkOption { type = nullOr path; - description = "Directory where downloading files are stored"; - defaultText = "/incomplete"; + description = "Directory where incomplete downloading files are stored."; + defaultText = "/var/lib/slskd/incomplete"; default = null; }; downloads = mkOption { type = nullOr path; - description = "Directory where downloaded files are stored"; - defaultText = "/downloads"; + description = "Directory where downloaded files are stored."; + defaultText = "/var/lib/slskd/downloads"; default = null; }; }; + + shares = { + directories = mkOption { + type = listOf str; + description = '' + Paths to shared directories. See + [documentation](https://github.com/slskd/slskd/blob/master/docs/config.md#directories) + for advanced usage. + ''; + example = lib.literalExpression ''[ "/home/John/Music" "!/home/John/Music/Recordings" "[Music Drive]/mnt" ]''; + }; + filters = mkOption { + type = listOf str; + example = lib.literalExpression ''[ "\.ini$" "Thumbs.db$" "\.DS_Store$" ]''; + description = "Regular expressions of files to exclude from sharing."; + }; + }; + + rooms = mkOption { + type = listOf str; + description = "Chat rooms to join on startup."; + }; + + soulseek = { + description = mkOption { + type = str; + description = "The user description for the Soulseek network."; + defaultText = "A slskd user. https://github.com/slskd/slskd"; + }; + listen_port = mkOption { + type = port; + description = "The port on which to listen for incoming connections."; + default = 50300; + }; + }; + + global = { + # TODO speed units + upload = { + slots = mkOption { + type = ints.unsigned; + description = "Limit of the number of concurrent upload slots."; + }; + speed_limit = mkOption { + type = ints.unsigned; + description = "Total upload speed limit."; + }; + }; + download = { + slots = mkOption { + type = ints.unsigned; + description = "Limit of the number of concurrent download slots."; + }; + speed_limit = mkOption { + type = ints.unsigned; + description = "Total upload download limit"; + }; + }; + }; + + filters.search.request = mkOption { + type = listOf str; + example = lib.literalExpression ''[ "^.{1,2}$" ]''; + description = "Incoming search requests which match this filter are ignored."; + }; + + web = { + port = mkOption { + type = port; + default = 5030; + description = "The HTTP listen port."; + }; + url_base = mkOption { + type = path; + default = "/"; + description = "The base path in the url for web requests."; + }; + # Users should use a reverse proxy instead for https + https.disabled = mkOption { + type = bool; + default = true; + description = "Disable the built-in HTTPS server"; + }; + }; + + retention = { + transfers = { + upload = { + succeeded = mkOption { + type = ints.unsigned; + description = "Lifespan of succeeded upload tasks."; + defaultText = "(indefinite)"; + }; + errored = mkOption { + type = ints.unsigned; + description = "Lifespan of errored upload tasks."; + defaultText = "(indefinite)"; + }; + cancelled = mkOption { + type = ints.unsigned; + description = "Lifespan of cancelled upload tasks."; + defaultText = "(indefinite)"; + }; + }; + download = { + succeeded = mkOption { + type = ints.unsigned; + description = "Lifespan of succeeded download tasks."; + defaultText = "(indefinite)"; + }; + errored = mkOption { + type = ints.unsigned; + description = "Lifespan of errored download tasks."; + defaultText = "(indefinite)"; + }; + cancelled = mkOption { + type = ints.unsigned; + description = "Lifespan of cancelled download tasks."; + defaultText = "(indefinite)"; + }; + }; + }; + files = { + complete = mkOption { + type = ints.unsigned; + description = "Lifespan of completely downloaded files in minutes."; + example = 20160; + defaultText = "(indefinite)"; + }; + incomplete = mkOption { + type = ints.unsigned; + description = "Lifespan of incomplete downloading files in minutes."; + defaultText = "(indefinite)"; + }; + }; + }; + + logger = { + # Disable by default, journald already retains as needed + disk = mkOption { + type = bool; + description = "Whether to log to the application directory."; + default = false; + visible = false; + }; + }; }; }; }; @@ -126,38 +254,25 @@ in { config = let cfg = config.services.slskd; - confWithoutNullValues = (lib.filterAttrs (key: value: value != null) cfg.settings); + confWithoutNullValues = (lib.filterAttrsRecursive (key: value: (builtins.tryEval value).success && value != null) cfg.settings); configurationYaml = settingsFormat.generate "slskd.yml" confWithoutNullValues; in lib.mkIf cfg.enable { - users = { - users.slskd = { + # Force off, configuration file is in nix store and is immutable + services.slskd.settings.remote_configuration = lib.mkForce false; + + users.users = lib.optionalAttrs (cfg.user == defaultUser) { + "${defaultUser}" = { + group = cfg.group; isSystemUser = true; - group = "slskd"; - }; - groups.slskd = {}; - }; - - # Reverse proxy configuration - services.nginx.enable = true; - services.nginx.virtualHosts."${cfg.nginx.domainName}" = { - forceSSL = true; - enableACME = true; - locations = { - "${cfg.nginx.contextPath}" = { - proxyPass = "http://localhost:${toString cfg.settings.web.port}"; - proxyWebsockets = true; - }; }; }; - # Hide state & logs - systemd.tmpfiles.rules = [ - "d /var/lib/slskd/data 0750 slskd slskd - -" - "d /var/lib/slskd/logs 0750 slskd slskd - -" - ]; + users.groups = lib.optionalAttrs (cfg.group == defaultUser) { + "${defaultUser}" = {}; + }; systemd.services.slskd = { description = "A modern client-server application for the Soulseek file sharing network"; @@ -165,12 +280,16 @@ in { wantedBy = [ "multi-user.target" ]; serviceConfig = { Type = "simple"; - User = "slskd"; + User = cfg.user; + Group = cfg.group; EnvironmentFile = lib.mkIf (cfg.environmentFile != null) cfg.environmentFile; - StateDirectory = "slskd"; + StateDirectory = "slskd"; # Creates /var/lib/slskd and manages permissions ExecStart = "${cfg.package}/bin/slskd --app-dir /var/lib/slskd --config ${configurationYaml}"; Restart = "on-failure"; ReadOnlyPaths = map (d: builtins.elemAt (builtins.split "[^/]*(/.+)" d) 1) cfg.settings.shares.directories; + ReadWritePaths = + (lib.optional (cfg.settings.directories.incomplete != null) cfg.settings.directories.incomplete) ++ + (lib.optional (cfg.settings.directories.downloads != null) cfg.settings.directories.downloads); LockPersonality = true; NoNewPrivileges = true; PrivateDevices = true; @@ -194,18 +313,21 @@ in { networking.firewall.allowedTCPPorts = lib.optional cfg.openFirewall cfg.settings.soulseek.listen_port; - systemd.services.slskd-rotatelogs = lib.mkIf cfg.rotateLogs { - description = "Rotate slskd logs"; - serviceConfig = { - Type = "oneshot"; - User = "slskd"; - ExecStart = [ - "${pkgs.findutils}/bin/find /var/lib/slskd/logs/ -type f -mtime +10 -delete" - "${pkgs.findutils}/bin/find /var/lib/slskd/logs/ -type f -mtime +1 -exec ${pkgs.gzip}/bin/gzip -q {} ';'" - ]; - }; - startAt = "daily"; + services.nginx = lib.mkIf (cfg.domain != null) { + enable = lib.mkDefault true; + virtualHosts."${cfg.domain}" = lib.mkMerge [ + cfg.nginx + { + locations."${cfg.settings.web.url_base}" = { + proxyPass = "http://127.0.0.1:${toString cfg.settings.web.port}"; + proxyWebsockets = true; + }; + } + ]; }; + }; + meta = { + maintainers = with lib.maintainers; [ ppom melvyn2 ]; }; } diff --git a/pkgs/servers/web-apps/slskd/default.nix b/pkgs/servers/web-apps/slskd/default.nix index 7a2a388086c1..0a69acb5e08c 100644 --- a/pkgs/servers/web-apps/slskd/default.nix +++ b/pkgs/servers/web-apps/slskd/default.nix @@ -21,7 +21,7 @@ let description = "A modern client-server application for the Soulseek file sharing network"; homepage = "https://github.com/slskd/slskd"; license = licenses.agpl3Plus; - maintainers = with maintainers; [ ppom ]; + maintainers = with maintainers; [ ppom melvyn2 ]; platforms = platforms.linux; };