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 = lib.mdDoc ''
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 = lib.mdDoc ''
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 = lib.mdDoc ''
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 = lib.mdDoc ''
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 = lib.mdDoc ''
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 = lib.mdDoc ''
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 = lib.mdDoc ''
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 = lib.mdDoc ''
145 HTTP address to bind to.
146 '';
147 };
148
149 httpListenPort = mkOption {
150 type = types.int;
151 default = 9000;
152 description = lib.mdDoc ''
153 HTTP port to bind on.
154 '';
155 };
156
157 httpLogin = mkOption {
158 type = types.str;
159 example = "allyourbase";
160 default = "";
161 description = lib.mdDoc ''
162 HTTP web login username.
163 '';
164 };
165
166 httpPass = mkOption {
167 type = types.str;
168 example = "arebelongtous";
169 default = "";
170 description = lib.mdDoc ''
171 HTTP web login password.
172 '';
173 };
174
175 encryptLAN = mkOption {
176 type = types.bool;
177 default = true;
178 description = lib.mdDoc "Encrypt LAN data.";
179 };
180
181 enableWebUI = mkOption {
182 type = types.bool;
183 default = false;
184 description = lib.mdDoc ''
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 = lib.mdDoc ''
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 = lib.mdDoc "API key, which enables the developer API.";
205 };
206
207 directoryRoot = mkOption {
208 type = types.str;
209 default = "";
210 example = "/media";
211 description = lib.mdDoc "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 = lib.mdDoc ''
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}