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