diff --git a/nixos/modules/services/torrent/transmission.nix b/nixos/modules/services/torrent/transmission.nix
index 014a22bb5a8..6f6d5a05a2a 100644
--- a/nixos/modules/services/torrent/transmission.nix
+++ b/nixos/modules/services/torrent/transmission.nix
@@ -12,10 +12,15 @@ let
downloadsDir = "Downloads";
incompleteDir = ".incomplete";
watchDir = "watchdir";
- # TODO: switch to configGen.json once RFC0042 is implemented
settingsFile = pkgs.writeText "settings.json" (builtins.toJSON cfg.settings);
in
{
+ imports = [
+ (mkRenamedOptionModule ["services" "transmission" "port"]
+ ["services" "transmission" "settings" "rpc-port"])
+ (mkRenamedOptionModule ["services" "transmission" "openFirewall"]
+ ["services" "transmission" "openPeerPorts"])
+ ];
options = {
services.transmission = {
enable = mkEnableOption ''the headless Transmission BitTorrent daemon.
@@ -27,45 +32,140 @@ in
Torrents are downloaded to ${homeDir}/${downloadsDir} by default and are
accessible to users in the "transmission" group'';
- settings = mkOption rec {
- # TODO: switch to types.config.json as prescribed by RFC0042 once it's implemented
- type = types.attrs;
- apply = recursiveUpdate default;
- default =
- {
- download-dir = "${cfg.home}/${downloadsDir}";
- incomplete-dir = "${cfg.home}/${incompleteDir}";
- incomplete-dir-enabled = true;
- watch-dir = "${cfg.home}/${watchDir}";
- watch-dir-enabled = false;
- message-level = 1;
- peer-port = 51413;
- peer-port-random-high = 65535;
- peer-port-random-low = 49152;
- peer-port-random-on-start = false;
- rpc-bind-address = "127.0.0.1";
- rpc-port = 9091;
- script-torrent-done-enabled = false;
- script-torrent-done-filename = "";
- umask = 2; # 0o002 in decimal as expected by Transmission
- utp-enabled = true;
- };
- example =
- {
- download-dir = "/srv/torrents/";
- incomplete-dir = "/srv/torrents/.incomplete/";
- incomplete-dir-enabled = true;
- rpc-whitelist = "127.0.0.1,192.168.*.*";
- };
+ settings = mkOption {
description = ''
- Attribute set whose fields overwrites fields in
+ Settings whose fields overwrites fields in
.config/transmission-daemon/settings.json
- (each time the service starts). String values must be quoted, integer and
- boolean values must not.
+ (each time the service starts).
See Transmission's Wiki
- for documentation.
+ for documentation of settings not explicitely covered by this module.
'';
+ default = {};
+ type = types.submodule {
+ freeformType = with types;
+ (attrsOf (nullOr (oneOf [str int bool]))) // {
+ description = "setting option";
+ };
+ options.download-dir = mkOption {
+ type = types.path;
+ default = "${cfg.home}/${downloadsDir}";
+ description = "Directory where to download torrents.";
+ };
+ options.incomplete-dir = mkOption {
+ type = types.path;
+ default = "${cfg.home}/${incompleteDir}";
+ description = ''
+ When enabled with
+ incomplete-dir-enabled,
+ new torrents will download the files to this directory.
+ When complete, the files will be moved to download-dir
+ download-dir.
+ '';
+ };
+ options.incomplete-dir-enabled = mkOption {
+ type = types.bool;
+ default = true;
+ description = "";
+ };
+ options.message-level = mkOption {
+ type = types.ints.between 0 2;
+ default = 2;
+ description = "Set verbosity of transmission messages.";
+ };
+ options.peer-port = mkOption {
+ type = types.port;
+ default = 51413;
+ description = "The peer port to listen for incoming connections.";
+ };
+ options.peer-port-random-high = mkOption {
+ type = types.port;
+ default = 65535;
+ description = ''
+ The maximum peer port to listen to for incoming connections
+ when peer-port-random-on-start is enabled.
+ '';
+ };
+ options.peer-port-random-low = mkOption {
+ type = types.port;
+ default = 65535;
+ description = ''
+ The minimal peer port to listen to for incoming connections
+ when peer-port-random-on-start is enabled.
+ '';
+ };
+ options.peer-port-random-on-start = mkOption {
+ type = types.bool;
+ default = false;
+ description = "Randomize the peer port.";
+ };
+ options.rpc-bind-address = mkOption {
+ type = types.str;
+ default = "127.0.0.1";
+ example = "0.0.0.0";
+ description = ''
+ Where to listen for RPC connections.
+ Use \"0.0.0.0\" to listen on all interfaces.
+ '';
+ };
+ options.rpc-port = mkOption {
+ type = types.port;
+ default = 9091;
+ description = "The RPC port to listen to.";
+ };
+ options.script-torrent-done-enabled = mkOption {
+ type = types.bool;
+ default = false;
+ description = ''
+ Whether to run
+ script-torrent-done-filename
+ at torrent completion.
+ '';
+ };
+ options.script-torrent-done-filename = mkOption {
+ type = types.nullOr types.path;
+ default = null;
+ description = "Executable to be run at torrent completion.";
+ };
+ options.umask = mkOption {
+ type = types.int;
+ default = 2;
+ description = ''
+ Sets transmission's file mode creation mask.
+ See the umask(2) manpage for more information.
+ Users who want their saved torrents to be world-writable
+ may want to set this value to 0.
+ Bear in mind that the json markup language only accepts numbers in base 10,
+ so the standard umask(2) octal notation "022" is written in settings.json as 18.
+ '';
+ };
+ options.utp-enabled = mkOption {
+ type = types.bool;
+ default = true;
+ description = ''
+ Whether to enable Micro Transport Protocol (µTP).
+ '';
+ };
+ options.watch-dir = mkOption {
+ type = types.path;
+ default = "${cfg.home}/${watchDir}";
+ description = "Watch a directory for torrent files and add them to transmission.";
+ };
+ options.watch-dir-enabled = mkOption {
+ type = types.bool;
+ default = false;
+ description = ''Whether to enable the
+ watch-dir.
+ '';
+ };
+ options.trash-original-torrent-files = mkOption {
+ type = types.bool;
+ default = false;
+ description = ''Whether to delete torrents added from the
+ watch-dir.
+ '';
+ };
+ };
};
downloadDirPermissions = mkOption {
@@ -81,17 +181,6 @@ in
'';
};
- port = mkOption {
- type = types.port;
- description = ''
- TCP port number to run the RPC/web interface.
-
- If instead you want to change the peer port,
- use settings.peer-port
- or settings.peer-port-random-on-start.
- '';
- };
-
home = mkOption {
type = types.path;
default = homeDir;
@@ -125,7 +214,9 @@ in
example = "/var/lib/secrets/transmission/settings.json";
};
- openFirewall = mkEnableOption "opening of the peer port(s) in the firewall";
+ openPeerPorts = mkEnableOption "opening of the peer port(s) in the firewall";
+
+ openRPCPort = mkEnableOption "opening of the RPC port in the firewall";
performanceNetParameters = mkEnableOption ''tweaking of kernel parameters
to open many more connections at the same time.
@@ -152,36 +243,10 @@ in
install -d -m '${cfg.downloadDirPermissions}' -o '${cfg.user}' -g '${cfg.group}' '${cfg.settings.download-dir}'
'' + optionalString cfg.settings.incomplete-dir-enabled ''
install -d -m '${cfg.downloadDirPermissions}' -o '${cfg.user}' -g '${cfg.group}' '${cfg.settings.incomplete-dir}'
+ '' + optionalString cfg.settings.watch-dir-enabled ''
+ install -d -m '${cfg.downloadDirPermissions}' -o '${cfg.user}' -g '${cfg.group}' '${cfg.settings.watch-dir}'
'';
- assertions = [
- { assertion = builtins.match "^/.*" cfg.home != null;
- message = "`services.transmission.home' must be an absolute path.";
- }
- { assertion = types.path.check cfg.settings.download-dir;
- message = "`services.transmission.settings.download-dir' must be an absolute path.";
- }
- { assertion = types.path.check cfg.settings.incomplete-dir;
- message = "`services.transmission.settings.incomplete-dir' must be an absolute path.";
- }
- { assertion = types.path.check cfg.settings.watch-dir;
- message = "`services.transmission.settings.watch-dir' must be an absolute path.";
- }
- { assertion = cfg.settings.script-torrent-done-filename == "" || types.path.check cfg.settings.script-torrent-done-filename;
- message = "`services.transmission.settings.script-torrent-done-filename' must be an absolute path.";
- }
- { assertion = types.port.check cfg.settings.rpc-port;
- message = "${toString cfg.settings.rpc-port} is not a valid port number for `services.transmission.settings.rpc-port`.";
- }
- # In case both port and settings.rpc-port are explicitely defined: they must be the same.
- { assertion = !options.services.transmission.port.isDefined || cfg.port == cfg.settings.rpc-port;
- message = "`services.transmission.port' is not equal to `services.transmission.settings.rpc-port'";
- }
- ];
-
- services.transmission.settings =
- optionalAttrs options.services.transmission.port.isDefined { rpc-port = cfg.port; };
-
systemd.services.transmission = {
description = "Transmission BitTorrent Service";
after = [ "network.target" ] ++ optional apparmor "apparmor.service";
@@ -226,11 +291,9 @@ in
cfg.settings.download-dir
] ++
optional cfg.settings.incomplete-dir-enabled
- cfg.settings.incomplete-dir
- ++
- optional cfg.settings.watch-dir-enabled
- cfg.settings.watch-dir
- ;
+ cfg.settings.incomplete-dir ++
+ optional (cfg.settings.watch-dir-enabled && cfg.settings.trash-original-torrent-files)
+ cfg.settings.watch-dir;
BindReadOnlyPaths = [
# No confinement done of /nix/store here like in systemd-confinement.nix,
# an AppArmor profile is provided to get a confinement based upon paths and rights.
@@ -238,8 +301,10 @@ in
"/etc"
] ++
optional (cfg.settings.script-torrent-done-enabled &&
- cfg.settings.script-torrent-done-filename != "")
- cfg.settings.script-torrent-done-filename;
+ cfg.settings.script-torrent-done-filename != null)
+ cfg.settings.script-torrent-done-filename ++
+ optional (cfg.settings.watch-dir-enabled && !cfg.settings.trash-original-torrent-files)
+ cfg.settings.watch-dir;
# The following options are only for optimizing:
# systemd-analyze security transmission
AmbientCapabilities = "";
@@ -306,25 +371,28 @@ in
};
});
- networking.firewall = mkIf cfg.openFirewall (
- if cfg.settings.peer-port-random-on-start
- then
- { allowedTCPPortRanges =
- [ { from = cfg.settings.peer-port-random-low;
- to = cfg.settings.peer-port-random-high;
- }
- ];
- allowedUDPPortRanges =
- [ { from = cfg.settings.peer-port-random-low;
- to = cfg.settings.peer-port-random-high;
- }
- ];
- }
- else
- { allowedTCPPorts = [ cfg.settings.peer-port ];
- allowedUDPPorts = [ cfg.settings.peer-port ];
- }
- );
+ networking.firewall = mkMerge [
+ (mkIf cfg.openPeerPorts (
+ if cfg.settings.peer-port-random-on-start
+ then
+ { allowedTCPPortRanges =
+ [ { from = cfg.settings.peer-port-random-low;
+ to = cfg.settings.peer-port-random-high;
+ }
+ ];
+ allowedUDPPortRanges =
+ [ { from = cfg.settings.peer-port-random-low;
+ to = cfg.settings.peer-port-random-high;
+ }
+ ];
+ }
+ else
+ { allowedTCPPorts = [ cfg.settings.peer-port ];
+ allowedUDPPorts = [ cfg.settings.peer-port ];
+ }
+ ))
+ (mkIf cfg.openRPCPort { allowedTCPPorts = [ cfg.settings.rpc-port ]; })
+ ];
boot.kernel.sysctl = mkMerge [
# Transmission uses a single UDP socket in order to implement multiple uTP sockets,
@@ -419,7 +487,7 @@ in
rw ${cfg.settings.incomplete-dir}/**,
''}
${optionalString cfg.settings.watch-dir-enabled ''
- rw ${cfg.settings.watch-dir}/**,
+ r${optionalString cfg.settings.trash-original-torrent-files "w"} ${cfg.settings.watch-dir}/**,
''}
profile dirs {
rw ${cfg.settings.download-dir}/**,
@@ -427,12 +495,12 @@ in
rw ${cfg.settings.incomplete-dir}/**,
''}
${optionalString cfg.settings.watch-dir-enabled ''
- rw ${cfg.settings.watch-dir}/**,
+ r${optionalString cfg.settings.trash-original-torrent-files "w"} ${cfg.settings.watch-dir}/**,
''}
}
${optionalString (cfg.settings.script-torrent-done-enabled &&
- cfg.settings.script-torrent-done-filename != "") ''
+ cfg.settings.script-torrent-done-filename != null) ''
# Stack transmission_directories profile on top of
# any existing profile for script-torrent-done-filename
# FIXME: to be tested as I'm not sure it works well with NoNewPrivileges=