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