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