about summary refs log tree commit diff
path: root/nixos/modules/services/web-apps/slskd.nix
diff options
context:
space:
mode:
Diffstat (limited to 'nixos/modules/services/web-apps/slskd.nix')
-rw-r--r--nixos/modules/services/web-apps/slskd.nix326
1 files changed, 224 insertions, 102 deletions
diff --git a/nixos/modules/services/web-apps/slskd.nix b/nixos/modules/services/web-apps/slskd.nix
index 580f66ec3ac90..15a5fd1177adf 100644
--- a/nixos/modules/services/web-apps/slskd.nix
+++ b/nixos/modules/services/web-apps/slskd.nix
@@ -2,120 +2,248 @@
 
 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.";
+    };
 
-    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";
+    group = mkOption {
+      type = types.str;
+      default = defaultUser;
+      description = "Group under which slskd runs.";
+    };
 
-          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 '/'
-            '';
-          };
-        };
-      });
+    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 {
+      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";
+
+          flags = {
+            force_share_scan = mkOption {
+              type = bool;
+              description = "Force a rescan of shares on every startup.";
+            };
+            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 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 = "/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 = {
-            username = mkOption {
+            description = mkOption {
               type = str;
-              description = "Username on the Soulseek Network";
+              description = "The user description for the Soulseek network.";
+              defaultText = "A slskd user. https://github.com/slskd/slskd";
             };
             listen_port = mkOption {
               type = port;
-              description = "Port to use for communication on the Soulseek Network";
-              default = 50000;
+              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 = 5001;
-              description = "The HTTP listen port";
+              default = 5030;
+              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
-              '';
+              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";
             };
           };
 
-          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
-              '';
+          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)";
+              };
             };
           };
 
-          directories = {
-            incomplete = mkOption {
-              type = nullOr path;
-              description = "Directory where downloading files are stored";
-              defaultText = "<APP_DIR>/incomplete";
-              default = null;
-            };
-            downloads = mkOption {
-              type = nullOr path;
-              description = "Directory where downloaded files are stored";
-              defaultText = "<APP_DIR>/downloads";
-              default = null;
+          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,51 +254,42 @@ 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;
-        };
-      };
+    users.groups = lib.optionalAttrs (cfg.group == defaultUser) {
+      "${defaultUser}" = {};
     };
 
-    # Hide state & logs
-    systemd.tmpfiles.rules = [
-      "d /var/lib/slskd/data 0750 slskd slskd - -"
-      "d /var/lib/slskd/logs 0750 slskd slskd - -"
-    ];
-
     systemd.services.slskd = {
       description = "A modern client-server application for the Soulseek file sharing network";
       after = [ "network.target" ];
       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 ];
   };
 }