base16-schemes: unstable-2024-06-21 -> unstable-2024-11-12
[NixPkgs.git] / nixos / modules / services / misc / anki-sync-server.nix
blobc77afe8c38199915ba17a796946478e77456bfc2
2   config,
3   lib,
4   pkgs,
5   ...
6 }:
7 with 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   '';
41 in {
42   options.services.anki-sync-server = {
43     enable = mkEnableOption "anki-sync-server";
45     package = mkPackageOption pkgs "anki-sync-server" { };
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     };
56     port = mkOption {
57       type = types.port;
58       default = 27701;
59       description = "Port number anki-sync-server listens to.";
60     };
62     baseDirectory = mkOption {
63       type = types.str;
64       default = "%S/%N";
65       description = "Base directory where user(s) synchronized data will be stored.";
66     };
69     openFirewall = mkOption {
70       default = false;
71       type = types.bool;
72       description = "Whether to open the firewall for the specified port.";
73     };
75     users = mkOption {
76       type = with types;
77         listOf (submodule {
78           options = {
79             username = mkOption {
80               type = str;
81               description = "User name accepted by anki-sync-server.";
82             };
83             password = mkOption {
84               type = nullOr str;
85               default = null;
86               description = ''
87                 Password accepted by anki-sync-server for the associated username.
88                 **WARNING**: This option is **not secure**. This password will
89                 be stored in *plaintext* and will be visible to *all users*.
90                 See {option}`services.anki-sync-server.users.passwordFile` for
91                 a more secure option.
92               '';
93             };
94             passwordFile = mkOption {
95               type = nullOr path;
96               default = null;
97               description = ''
98                 File containing the password accepted by anki-sync-server for
99                 the associated username.  Make sure to make readable only by
100                 root.
101               '';
102             };
103           };
104         });
105       description = "List of user-password pairs to provide to the sync server.";
106     };
107   };
109   config = mkIf cfg.enable {
110     assertions = [
111       {
112         assertion = (builtins.length usersWithIndexesFile) + (builtins.length usersWithIndexesNoFile) > 0;
113         message = "At least one username-password pair must be set.";
114       }
115     ];
116     networking.firewall.allowedTCPPorts = mkIf cfg.openFirewall [cfg.port];
118     systemd.services.anki-sync-server = {
119       description = "anki-sync-server: Anki sync server built into Anki";
120       after = ["network.target"];
121       wantedBy = ["multi-user.target"];
122       path = [cfg.package];
123       environment = {
124         SYNC_BASE = cfg.baseDirectory;
125         SYNC_HOST = specEscape cfg.address;
126         SYNC_PORT = toString cfg.port;
127       };
129       serviceConfig = {
130         Type = "simple";
131         DynamicUser = true;
132         StateDirectory = name;
133         ExecStart = "${anki-sync-server-run}/bin/anki-sync-server-run";
134         Restart = "always";
135         LoadCredential =
136           map
137           (x: "${specEscape x.user.username}:${specEscape (toString x.user.passwordFile)}")
138           usersWithIndexesFile;
139       };
140     };
141   };
143   meta = {
144     maintainers = with maintainers; [telotortium];
145     doc = ./anki-sync-server.md;
146   };