summary refs log tree commit diff
path: root/nixos/modules/services/torrent/transmission.nix
blob: aa1acdf7d20b98513d6e6ed6e7d1a24ac822bb35 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
{ config, lib, pkgs, ... }:

with lib;

let
  cfg = config.services.transmission;
  apparmor = config.security.apparmor.enable;

  homeDir = cfg.home;
  downloadDirPermissions = cfg.downloadDirPermissions;
  downloadDir = "${homeDir}/Downloads";
  incompleteDir = "${homeDir}/.incomplete";

  settingsDir = "${homeDir}/.config/transmission-daemon";
  settingsFile = pkgs.writeText "settings.json" (builtins.toJSON fullSettings);

  # for users in group "transmission" to have access to torrents
  fullSettings = { umask = 2; download-dir = downloadDir; incomplete-dir = incompleteDir; } // cfg.settings;

  preStart = pkgs.writeScript "transmission-pre-start" ''
    #!${pkgs.runtimeShell}
    set -ex
    for DIR in "${homeDir}" "${settingsDir}" "${fullSettings.download-dir}" "${fullSettings.incomplete-dir}"; do
      mkdir -p "$DIR"
    done
    chmod 700 "${homeDir}" "${settingsDir}"
    chmod ${downloadDirPermissions} "${fullSettings.download-dir}" "${fullSettings.incomplete-dir}"
    cp -f ${settingsFile} ${settingsDir}/settings.json
  '';
in
{
  options = {
    services.transmission = {
      enable = mkOption {
        type = types.bool;
        default = false;
        description = ''
          Whether or not to enable the headless Transmission BitTorrent daemon.

          Transmission daemon can be controlled via the RPC interface using
          transmission-remote or the WebUI (http://localhost:9091/ by default).

          Torrents are downloaded to ${downloadDir} by default and are
          accessible to users in the "transmission" group.
        '';
      };

      settings = mkOption {
        type = types.attrs;
        default =
          {
            download-dir = downloadDir;
            incomplete-dir = incompleteDir;
            incomplete-dir-enabled = true;
          };
        example =
          {
            download-dir = "/srv/torrents/";
            incomplete-dir = "/srv/torrents/.incomplete/";
            incomplete-dir-enabled = true;
            rpc-whitelist = "127.0.0.1,192.168.*.*";
          };
        description = ''
          Attribute set whos fields overwrites fields in settings.json (each
          time the service starts). String values must be quoted, integer and
          boolean values must not.

          See https://github.com/transmission/transmission/wiki/Editing-Configuration-Files
          for documentation.
        '';
      };

      downloadDirPermissions = mkOption {
        type = types.str;
        default = "770";
        example = "775";
        description = ''
          The permissions to set for download-dir and incomplete-dir.
          They will be applied on every service start.
        '';
      };

      port = mkOption {
        type = types.int;
        default = 9091;
        description = "TCP port number to run the RPC/web interface.";
      };

      home = mkOption {
        type = types.path;
        default = "/var/lib/transmission";
        description = ''
          The directory where transmission will create files.
        '';
      };

      user = mkOption {
        type = types.str;
        default = "transmission";
        description = "User account under which Transmission runs.";
      };

      group = mkOption {
        type = types.str;
        default = "transmission";
        description = "Group account under which Transmission runs.";
      };
    };
  };

  config = mkIf cfg.enable {
    systemd.services.transmission = {
      description = "Transmission BitTorrent Service";
      after = [ "network.target" ] ++ optional apparmor "apparmor.service";
      requires = mkIf apparmor [ "apparmor.service" ];
      wantedBy = [ "multi-user.target" ];

      # 1) Only the "transmission" user and group have access to torrents.
      # 2) Optionally update/force specific fields into the configuration file.
      serviceConfig.ExecStartPre = preStart;
      serviceConfig.ExecStart = "${pkgs.transmission}/bin/transmission-daemon -f --port ${toString config.services.transmission.port} --config-dir ${settingsDir}";
      serviceConfig.ExecReload = "${pkgs.coreutils}/bin/kill -HUP $MAINPID";
      serviceConfig.User = cfg.user;
      serviceConfig.Group = cfg.group;
      # NOTE: transmission has an internal umask that also must be set (in settings.json)
      serviceConfig.UMask = "0002";
    };

    # It's useful to have transmission in path, e.g. for remote control
    environment.systemPackages = [ pkgs.transmission ];

    users.users = optionalAttrs (cfg.user == "transmission") (singleton
      { name = "transmission";
        group = cfg.group;
        uid = config.ids.uids.transmission;
        description = "Transmission BitTorrent user";
        home = homeDir;
        createHome = true;
      });

    users.groups = optionalAttrs (cfg.group == "transmission") (singleton
      { name = "transmission";
        gid = config.ids.gids.transmission;
      });

    # AppArmor profile
    security.apparmor.profiles = mkIf apparmor [
      (pkgs.writeText "apparmor-transmission-daemon" ''
        #include <tunables/global>

        ${pkgs.transmission}/bin/transmission-daemon {
          #include <abstractions/base>
          #include <abstractions/nameservice>

          ${getLib pkgs.glibc}/lib/*.so                    mr,
          ${getLib pkgs.libevent}/lib/libevent*.so*        mr,
          ${getLib pkgs.curl}/lib/libcurl*.so*             mr,
          ${getLib pkgs.openssl}/lib/libssl*.so*           mr,
          ${getLib pkgs.openssl}/lib/libcrypto*.so*        mr,
          ${getLib pkgs.zlib}/lib/libz*.so*                mr,
          ${getLib pkgs.libssh2}/lib/libssh2*.so*          mr,
          ${getLib pkgs.systemd}/lib/libsystemd*.so*       mr,
          ${getLib pkgs.xz}/lib/liblzma*.so*               mr,
          ${getLib pkgs.libgcrypt}/lib/libgcrypt*.so*      mr,
          ${getLib pkgs.libgpgerror}/lib/libgpg-error*.so* mr,
          ${getLib pkgs.nghttp2}/lib/libnghttp2*.so*       mr,
          ${getLib pkgs.c-ares}/lib/libcares*.so*          mr,
          ${getLib pkgs.libcap}/lib/libcap*.so*            mr,
          ${getLib pkgs.attr}/lib/libattr*.so*             mr,
          ${getLib pkgs.lz4}/lib/liblz4*.so*               mr,
          ${getLib pkgs.libkrb5}/lib/lib*.so*              mr,
          ${getLib pkgs.keyutils}/lib/libkeyutils*.so*     mr,
          ${getLib pkgs.utillinuxMinimal.out}/lib/libblkid.so.* mr,
          ${getLib pkgs.utillinuxMinimal.out}/lib/libmount.so.* mr,
          ${getLib pkgs.utillinuxMinimal.out}/lib/libuuid.so.* mr,

          @{PROC}/sys/kernel/random/uuid   r,
          @{PROC}/sys/vm/overcommit_memory r,

          ${pkgs.openssl.out}/etc/**                     r,
          ${pkgs.transmission}/share/transmission/** r,

          owner ${settingsDir}/** rw,

          ${fullSettings.download-dir}/** rw,
          ${optionalString fullSettings.incomplete-dir-enabled ''
            ${fullSettings.incomplete-dir}/** rw,
          ''}
        }
      '')
    ];
  };

}