1{
2 config,
3 lib,
4 pkgs,
5 ...
6}:
7with lib; let
8 cfg = config.services.anki-sync-server;
9 name = "anki-sync-server";
10 specEscape = replaceStrings ["%"] ["%%"];
11 usersWithIndexes =
12 lists.imap1 (i: user: {
13 i = i;
14 user = user;
15 })
16 cfg.users;
17 usersWithIndexesFile = filter (x: x.user.passwordFile != null) usersWithIndexes;
18 usersWithIndexesNoFile = filter (x: x.user.passwordFile == null && x.user.password != null) usersWithIndexes;
19 anki-sync-server-run = pkgs.writeShellScriptBin "anki-sync-server-run" ''
20 # When services.anki-sync-server.users.passwordFile is set,
21 # each password file is passed as a systemd credential, which is mounted in
22 # a file system exposed to the service. Here we read the passwords from
23 # the credential files to pass them as environment variables to the Anki
24 # sync server.
25 ${
26 concatMapStringsSep
27 "\n"
28 (x: ''export SYNC_USER${toString x.i}=${escapeShellArg x.user.username}:"''$(cat "''${CREDENTIALS_DIRECTORY}/"${escapeShellArg x.user.username})"'')
29 usersWithIndexesFile
30 }
31 # For users where services.anki-sync-server.users.password isn't set,
32 # export passwords in environment variables in plaintext.
33 ${
34 concatMapStringsSep
35 "\n"
36 (x: ''export SYNC_USER${toString x.i}=${escapeShellArg x.user.username}:${escapeShellArg x.user.password}'')
37 usersWithIndexesNoFile
38 }
39 exec ${cfg.package}/bin/anki-sync-server
40 '';
41in {
42 options.services.anki-sync-server = {
43 enable = mkEnableOption "anki-sync-server";
44
45 package = mkPackageOption pkgs "anki-sync-server" { };
46
47 address = mkOption {
48 type = types.str;
49 default = "::1";
50 description = ''
51 IP address anki-sync-server listens to.
52 Note host names are not resolved.
53 '';
54 };
55
56 port = mkOption {
57 type = types.port;
58 default = 27701;
59 description = "Port number anki-sync-server listens to.";
60 };
61
62 openFirewall = mkOption {
63 default = false;
64 type = types.bool;
65 description = "Whether to open the firewall for the specified port.";
66 };
67
68 users = mkOption {
69 type = with types;
70 listOf (submodule {
71 options = {
72 username = mkOption {
73 type = str;
74 description = "User name accepted by anki-sync-server.";
75 };
76 password = mkOption {
77 type = nullOr str;
78 default = null;
79 description = ''
80 Password accepted by anki-sync-server for the associated username.
81 **WARNING**: This option is **not secure**. This password will
82 be stored in *plaintext* and will be visible to *all users*.
83 See {option}`services.anki-sync-server.users.passwordFile` for
84 a more secure option.
85 '';
86 };
87 passwordFile = mkOption {
88 type = nullOr path;
89 default = null;
90 description = ''
91 File containing the password accepted by anki-sync-server for
92 the associated username. Make sure to make readable only by
93 root.
94 '';
95 };
96 };
97 });
98 description = "List of user-password pairs to provide to the sync server.";
99 };
100 };
101
102 config = mkIf cfg.enable {
103 assertions = [
104 {
105 assertion = (builtins.length usersWithIndexesFile) + (builtins.length usersWithIndexesNoFile) > 0;
106 message = "At least one username-password pair must be set.";
107 }
108 ];
109 networking.firewall.allowedTCPPorts = mkIf cfg.openFirewall [cfg.port];
110
111 systemd.services.anki-sync-server = {
112 description = "anki-sync-server: Anki sync server built into Anki";
113 after = ["network.target"];
114 wantedBy = ["multi-user.target"];
115 path = [cfg.package];
116 environment = {
117 SYNC_BASE = "%S/%N";
118 SYNC_HOST = specEscape cfg.address;
119 SYNC_PORT = toString cfg.port;
120 };
121
122 serviceConfig = {
123 Type = "simple";
124 DynamicUser = true;
125 StateDirectory = name;
126 ExecStart = "${anki-sync-server-run}/bin/anki-sync-server-run";
127 Restart = "always";
128 LoadCredential =
129 map
130 (x: "${specEscape x.user.username}:${specEscape (toString x.user.passwordFile)}")
131 usersWithIndexesFile;
132 };
133 };
134 };
135
136 meta = {
137 maintainers = with maintainers; [telotortium];
138 doc = ./anki-sync-server.md;
139 };
140}