1{ config, lib, pkgs, ... }:
2
3with lib;
4
5let
6 cfg = config.services.resilio;
7
8 resilioSync = pkgs.resilio-sync;
9
10 sharedFoldersRecord = map (entry: {
11 dir = entry.directory;
12
13 use_relay_server = entry.useRelayServer;
14 use_tracker = entry.useTracker;
15 use_dht = entry.useDHT;
16
17 search_lan = entry.searchLAN;
18 use_sync_trash = entry.useSyncTrash;
19 known_hosts = entry.knownHosts;
20 }) cfg.sharedFolders;
21
22 configFile = pkgs.writeText "config.json" (builtins.toJSON ({
23 device_name = cfg.deviceName;
24 storage_path = cfg.storagePath;
25 listening_port = cfg.listeningPort;
26 use_gui = false;
27 check_for_updates = cfg.checkForUpdates;
28 use_upnp = cfg.useUpnp;
29 download_limit = cfg.downloadLimit;
30 upload_limit = cfg.uploadLimit;
31 lan_encrypt_data = cfg.encryptLAN;
32 } // optionalAttrs (cfg.directoryRoot != "") { directory_root = cfg.directoryRoot; }
33 // optionalAttrs cfg.enableWebUI {
34 webui = { listen = "${cfg.httpListenAddr}:${toString cfg.httpListenPort}"; } //
35 (optionalAttrs (cfg.httpLogin != "") { login = cfg.httpLogin; }) //
36 (optionalAttrs (cfg.httpPass != "") { password = cfg.httpPass; }) //
37 (optionalAttrs (cfg.apiKey != "") { api_key = cfg.apiKey; });
38 } // optionalAttrs (sharedFoldersRecord != []) {
39 shared_folders = sharedFoldersRecord;
40 }));
41
42 sharedFoldersSecretFiles = map (entry: {
43 dir = entry.directory;
44 secretFile = if builtins.hasAttr "secret" entry then
45 toString (pkgs.writeTextFile {
46 name = "secret-file";
47 text = entry.secret;
48 })
49 else
50 entry.secretFile;
51 }) cfg.sharedFolders;
52
53 runConfigPath = "/run/rslsync/config.json";
54
55 createConfig = pkgs.writeShellScriptBin "create-resilio-config" (
56 if cfg.sharedFolders != [ ] then ''
57 ${pkgs.jq}/bin/jq \
58 '.shared_folders |= map(.secret = $ARGS.named[.dir])' \
59 ${
60 lib.concatMapStringsSep " \\\n "
61 (entry: ''--arg '${entry.dir}' "$(cat '${entry.secretFile}')"'')
62 sharedFoldersSecretFiles
63 } \
64 <${configFile} \
65 >${runConfigPath}
66 '' else ''
67 # no secrets, passing through config
68 cp ${configFile} ${runConfigPath};
69 ''
70 );
71
72in
73{
74 options = {
75 services.resilio = {
76 enable = mkOption {
77 type = types.bool;
78 default = false;
79 description = ''
80 If enabled, start the Resilio Sync daemon. Once enabled, you can
81 interact with the service through the Web UI, or configure it in your
82 NixOS configuration.
83 '';
84 };
85
86 deviceName = mkOption {
87 type = types.str;
88 example = "Voltron";
89 default = config.networking.hostName;
90 defaultText = literalExpression "config.networking.hostName";
91 description = ''
92 Name of the Resilio Sync device.
93 '';
94 };
95
96 listeningPort = mkOption {
97 type = types.int;
98 default = 0;
99 example = 44444;
100 description = ''
101 Listening port. Defaults to 0 which randomizes the port.
102 '';
103 };
104
105 checkForUpdates = mkOption {
106 type = types.bool;
107 default = true;
108 description = ''
109 Determines whether to check for updates and alert the user
110 about them in the UI.
111 '';
112 };
113
114 useUpnp = mkOption {
115 type = types.bool;
116 default = true;
117 description = ''
118 Use Universal Plug-n-Play (UPnP)
119 '';
120 };
121
122 downloadLimit = mkOption {
123 type = types.int;
124 default = 0;
125 example = 1024;
126 description = ''
127 Download speed limit. 0 is unlimited (default).
128 '';
129 };
130
131 uploadLimit = mkOption {
132 type = types.int;
133 default = 0;
134 example = 1024;
135 description = ''
136 Upload speed limit. 0 is unlimited (default).
137 '';
138 };
139
140 httpListenAddr = mkOption {
141 type = types.str;
142 default = "[::1]";
143 example = "0.0.0.0";
144 description = ''
145 HTTP address to bind to.
146 '';
147 };
148
149 httpListenPort = mkOption {
150 type = types.int;
151 default = 9000;
152 description = ''
153 HTTP port to bind on.
154 '';
155 };
156
157 httpLogin = mkOption {
158 type = types.str;
159 example = "allyourbase";
160 default = "";
161 description = ''
162 HTTP web login username.
163 '';
164 };
165
166 httpPass = mkOption {
167 type = types.str;
168 example = "arebelongtous";
169 default = "";
170 description = ''
171 HTTP web login password.
172 '';
173 };
174
175 encryptLAN = mkOption {
176 type = types.bool;
177 default = true;
178 description = "Encrypt LAN data.";
179 };
180
181 enableWebUI = mkOption {
182 type = types.bool;
183 default = false;
184 description = ''
185 Enable Web UI for administration. Bound to the specified
186 `httpListenAddress` and
187 `httpListenPort`.
188 '';
189 };
190
191 storagePath = mkOption {
192 type = types.path;
193 default = "/var/lib/resilio-sync/";
194 description = ''
195 Where BitTorrent Sync will store it's database files (containing
196 things like username info and licenses). Generally, you should not
197 need to ever change this.
198 '';
199 };
200
201 apiKey = mkOption {
202 type = types.str;
203 default = "";
204 description = "API key, which enables the developer API.";
205 };
206
207 directoryRoot = mkOption {
208 type = types.str;
209 default = "";
210 example = "/media";
211 description = "Default directory to add folders in the web UI.";
212 };
213
214 sharedFolders = mkOption {
215 default = [];
216 type = types.listOf (types.attrsOf types.anything);
217 example =
218 [ { secretFile = "/run/resilio-secret";
219 directory = "/home/user/sync_test";
220 useRelayServer = true;
221 useTracker = true;
222 useDHT = false;
223 searchLAN = true;
224 useSyncTrash = true;
225 knownHosts = [
226 "192.168.1.2:4444"
227 "192.168.1.3:4444"
228 ];
229 }
230 ];
231 description = ''
232 Shared folder list. If enabled, web UI must be
233 disabled. Secrets can be generated using `rslsync --generate-secret`.
234
235 If you would like to be able to modify the contents of this
236 directories, it is recommended that you make your user a
237 member of the `rslsync` group.
238
239 Directories in this list should be in the
240 `rslsync` group, and that group must have
241 write access to the directory. It is also recommended that
242 `chmod g+s` is applied to the directory
243 so that any sub directories created will also belong to
244 the `rslsync` group. Also,
245 `setfacl -d -m group:rslsync:rwx` and
246 `setfacl -m group:rslsync:rwx` should also
247 be applied so that the sub directories are writable by
248 the group.
249 '';
250 };
251 };
252 };
253
254 config = mkIf cfg.enable {
255 assertions =
256 [ { assertion = cfg.deviceName != "";
257 message = "Device name cannot be empty.";
258 }
259 { assertion = cfg.enableWebUI -> cfg.sharedFolders == [];
260 message = "If using shared folders, the web UI cannot be enabled.";
261 }
262 { assertion = cfg.apiKey != "" -> cfg.enableWebUI;
263 message = "If you're using an API key, you must enable the web server.";
264 }
265 ];
266
267 users.users.rslsync = {
268 description = "Resilio Sync Service user";
269 home = cfg.storagePath;
270 createHome = true;
271 uid = config.ids.uids.rslsync;
272 group = "rslsync";
273 };
274
275 users.groups.rslsync = {};
276
277 systemd.services.resilio = with pkgs; {
278 description = "Resilio Sync Service";
279 wantedBy = [ "multi-user.target" ];
280 after = [ "network.target" ];
281 serviceConfig = {
282 Restart = "on-abort";
283 UMask = "0002";
284 User = "rslsync";
285 RuntimeDirectory = "rslsync";
286 ExecStartPre = "${createConfig}/bin/create-resilio-config";
287 ExecStart = ''
288 ${resilioSync}/bin/rslsync --nodaemon --config ${runConfigPath}
289 '';
290 };
291 };
292 };
293
294 meta.maintainers = with maintainers; [ jwoudenberg ];
295}