grafana-alloy: don't build the frontend twice
[NixPkgs.git] / nixos / modules / services / web-apps / slskd.nix
blob7d4bc66c73998332a3f2ec678306b35cfb2648fe
1 { lib, pkgs, config, ... }:
3 let
4   settingsFormat = pkgs.formats.yaml {};
5   defaultUser = "slskd";
6 in {
7   options.services.slskd = with lib; with types; {
8     enable = mkEnableOption "slskd";
10     package = mkPackageOption pkgs "slskd" { };
12     user = mkOption {
13       type = types.str;
14       default = defaultUser;
15       description = "User account under which slskd runs.";
16     };
18     group = mkOption {
19       type = types.str;
20       default = defaultUser;
21       description = "Group under which slskd runs.";
22     };
24     domain = mkOption {
25       type = types.nullOr types.str;
26       description = ''
27         If non-null, enables an nginx reverse proxy virtual host at this FQDN,
28         at the path configurated with `services.slskd.web.url_base`.
29       '';
30       example = "slskd.example.com";
31     };
33     nginx = mkOption {
34       type = types.submodule (import ../web-servers/nginx/vhost-options.nix { inherit config lib; });
35       default = {};
36       example = lib.literalExpression ''
37         {
38           enableACME = true;
39           forceHttps = true;
40         }
41       '';
42       description = ''
43         This option customizes the nginx virtual host set up for slskd.
44       '';
45     };
47     environmentFile = mkOption {
48       type = path;
49       description = ''
50         Path to the environment file sourced on startup.
51         It must at least contain the variables `SLSKD_SLSK_USERNAME` and `SLSKD_SLSK_PASSWORD`.
52         Web interface credentials should also be set here in `SLSKD_USERNAME` and `SLSKD_PASSWORD`.
53         Other, optional credentials like SOCKS5 with `SLSKD_SLSK_PROXY_USERNAME` and `SLSKD_SLSK_PROXY_PASSWORD`
54         should all reside here instead of in the world-readable nix store.
55         Variables are documented at https://github.com/slskd/slskd/blob/master/docs/config.md
56       '';
57     };
59     openFirewall = mkOption {
60       type = bool;
61       description = "Whether to open the firewall for the soulseek network listen port (not the web interface port).";
62       default = false;
63     };
65     settings = mkOption {
66       description = ''
67         Application configuration for slskd. See
68         [documentation](https://github.com/slskd/slskd/blob/master/docs/config.md).
69       '';
70       default = {};
71       type = submodule {
72         freeformType = settingsFormat.type;
73         options = {
74           remote_file_management = mkEnableOption "modification of share contents through the web ui";
76           flags = {
77             force_share_scan = mkOption {
78               type = bool;
79               description = "Force a rescan of shares on every startup.";
80             };
81             no_version_check = mkOption {
82               type = bool;
83               default = true;
84               visible = false;
85               description = "Don't perform a version check on startup.";
86             };
87           };
89           directories = {
90             incomplete = mkOption {
91               type = nullOr path;
92               description = "Directory where incomplete downloading files are stored.";
93               defaultText = "/var/lib/slskd/incomplete";
94               default = null;
95             };
96             downloads = mkOption {
97               type = nullOr path;
98               description = "Directory where downloaded files are stored.";
99               defaultText = "/var/lib/slskd/downloads";
100               default = null;
101             };
102           };
104           shares = {
105             directories = mkOption {
106               type = listOf str;
107               description = ''
108                 Paths to shared directories. See
109                 [documentation](https://github.com/slskd/slskd/blob/master/docs/config.md#directories)
110                 for advanced usage.
111               '';
112               example = lib.literalExpression ''[ "/home/John/Music" "!/home/John/Music/Recordings" "[Music Drive]/mnt" ]'';
113             };
114             filters = mkOption {
115               type = listOf str;
116               example = lib.literalExpression ''[ "\.ini$" "Thumbs.db$" "\.DS_Store$" ]'';
117               description = "Regular expressions of files to exclude from sharing.";
118             };
119           };
121           rooms = mkOption {
122             type = listOf str;
123             description = "Chat rooms to join on startup.";
124           };
126           soulseek = {
127             description = mkOption {
128               type = str;
129               description = "The user description for the Soulseek network.";
130               defaultText = "A slskd user. https://github.com/slskd/slskd";
131             };
132             listen_port = mkOption {
133               type = port;
134               description = "The port on which to listen for incoming connections.";
135               default = 50300;
136             };
137           };
139           global = {
140             # TODO speed units
141             upload = {
142               slots = mkOption {
143                 type = ints.unsigned;
144                 description = "Limit of the number of concurrent upload slots.";
145               };
146               speed_limit = mkOption {
147                 type = ints.unsigned;
148                 description = "Total upload speed limit.";
149               };
150             };
151             download = {
152               slots = mkOption {
153                 type = ints.unsigned;
154                 description = "Limit of the number of concurrent download slots.";
155               };
156               speed_limit = mkOption {
157                 type = ints.unsigned;
158                 description = "Total upload download limit";
159               };
160             };
161           };
163           filters.search.request = mkOption {
164             type = listOf str;
165             example = lib.literalExpression ''[ "^.{1,2}$" ]'';
166             description = "Incoming search requests which match this filter are ignored.";
167           };
169           web = {
170             port = mkOption {
171               type = port;
172               default = 5030;
173               description = "The HTTP listen port.";
174             };
175             url_base = mkOption {
176               type = path;
177               default = "/";
178               description = "The base path in the url for web requests.";
179             };
180             # Users should use a reverse proxy instead for https
181             https.disabled = mkOption {
182               type = bool;
183               default = true;
184               description = "Disable the built-in HTTPS server";
185             };
186           };
188           retention = {
189             transfers = {
190               upload = {
191                 succeeded = mkOption {
192                   type = ints.unsigned;
193                   description = "Lifespan of succeeded upload tasks.";
194                   defaultText = "(indefinite)";
195                 };
196                 errored = mkOption {
197                   type = ints.unsigned;
198                   description = "Lifespan of errored upload tasks.";
199                   defaultText = "(indefinite)";
200                 };
201                 cancelled = mkOption {
202                   type = ints.unsigned;
203                   description = "Lifespan of cancelled upload tasks.";
204                   defaultText = "(indefinite)";
205                 };
206               };
207               download = {
208                 succeeded = mkOption {
209                   type = ints.unsigned;
210                   description = "Lifespan of succeeded download tasks.";
211                   defaultText = "(indefinite)";
212                 };
213                 errored = mkOption {
214                   type = ints.unsigned;
215                   description = "Lifespan of errored download tasks.";
216                   defaultText = "(indefinite)";
217                 };
218                 cancelled = mkOption {
219                   type = ints.unsigned;
220                   description = "Lifespan of cancelled download tasks.";
221                   defaultText = "(indefinite)";
222                 };
223               };
224             };
225             files = {
226               complete = mkOption {
227                 type = ints.unsigned;
228                 description = "Lifespan of completely downloaded files in minutes.";
229                 example = 20160;
230                 defaultText = "(indefinite)";
231               };
232               incomplete = mkOption {
233                 type = ints.unsigned;
234                 description = "Lifespan of incomplete downloading files in minutes.";
235                 defaultText = "(indefinite)";
236               };
237             };
238           };
240           logger = {
241             # Disable by default, journald already retains as needed
242             disk = mkOption {
243               type = bool;
244               description = "Whether to log to the application directory.";
245               default = false;
246               visible = false;
247             };
248           };
249         };
250       };
251     };
252   };
254   config = let
255     cfg = config.services.slskd;
257     confWithoutNullValues = (lib.filterAttrsRecursive (key: value: (builtins.tryEval value).success && value != null) cfg.settings);
259     configurationYaml = settingsFormat.generate "slskd.yml" confWithoutNullValues;
261   in lib.mkIf cfg.enable {
263     # Force off, configuration file is in nix store and is immutable
264     services.slskd.settings.remote_configuration = lib.mkForce false;
266     users.users = lib.optionalAttrs (cfg.user == defaultUser) {
267       "${defaultUser}" = {
268         group = cfg.group;
269         isSystemUser = true;
270       };
271     };
273     users.groups = lib.optionalAttrs (cfg.group == defaultUser) {
274       "${defaultUser}" = {};
275     };
277     systemd.services.slskd = {
278       description = "A modern client-server application for the Soulseek file sharing network";
279       after = [ "network.target" ];
280       wantedBy = [ "multi-user.target" ];
281       serviceConfig = {
282         Type = "simple";
283         User = cfg.user;
284         Group = cfg.group;
285         EnvironmentFile = lib.mkIf (cfg.environmentFile != null) cfg.environmentFile;
286         StateDirectory = "slskd";  # Creates /var/lib/slskd and manages permissions
287         ExecStart = "${cfg.package}/bin/slskd --app-dir /var/lib/slskd --config ${configurationYaml}";
288         Restart = "on-failure";
289         ReadOnlyPaths = map (d: builtins.elemAt (builtins.split "[^/]*(/.+)" d) 1) cfg.settings.shares.directories;
290         ReadWritePaths =
291           (lib.optional (cfg.settings.directories.incomplete != null) cfg.settings.directories.incomplete) ++
292           (lib.optional (cfg.settings.directories.downloads != null) cfg.settings.directories.downloads);
293         LockPersonality = true;
294         NoNewPrivileges = true;
295         PrivateDevices = true;
296         PrivateMounts = true;
297         PrivateTmp = true;
298         PrivateUsers = true;
299         ProtectClock = true;
300         ProtectControlGroups = true;
301         ProtectHome = true;
302         ProtectHostname = true;
303         ProtectKernelLogs = true;
304         ProtectKernelModules = true;
305         ProtectKernelTunables = true;
306         ProtectProc = "invisible";
307         ProtectSystem = "strict";
308         RemoveIPC = true;
309         RestrictNamespaces = true;
310         RestrictSUIDSGID = true;
311       };
312     };
314     networking.firewall.allowedTCPPorts = lib.optional cfg.openFirewall cfg.settings.soulseek.listen_port;
316     services.nginx = lib.mkIf (cfg.domain != null) {
317       enable = lib.mkDefault true;
318       virtualHosts."${cfg.domain}" = lib.mkMerge [
319         cfg.nginx
320         {
321           locations."${cfg.settings.web.url_base}" = {
322             proxyPass = "http://127.0.0.1:${toString cfg.settings.web.port}";
323             proxyWebsockets = true;
324           };
325         }
326       ];
327     };
328   };
330   meta = {
331     maintainers = with lib.maintainers; [ ppom melvyn2 ];
332   };