nixos/anki-sync-server: init

Provide a NixOS module for the [built-in Anki Sync
Server](https://docs.ankiweb.net/sync-server.html) included in recent
versions of Anki. This supersedes the `ankisyncd` module, but we should
keep that for now because `ankisyncd` supports older versions of Anki
clients than this module.

Changed files
+217
nixos
doc
manual
release-notes
modules
+2
nixos/doc/manual/release-notes/rl-2405.section.md
···
- [maubot](https://github.com/maubot/maubot), a plugin-based Matrix bot framework. Available as [services.maubot](#opt-services.maubot.enable).
+
- [Anki Sync Server](https://docs.ankiweb.net/sync-server.html), the official sync server built into recent versions of Anki. Available as [services.anki-sync-server](#opt-services.anki-sync-server.enable).
+
## Backward Incompatibilities {#sec-release-24.05-incompatibilities}
<!-- To avoid merge conflicts, consider adding your item at an arbitrary place in the list instead. -->
+1
nixos/modules/module-list.nix
···
./services/misc/amazon-ssm-agent.nix
./services/misc/ananicy.nix
./services/misc/ankisyncd.nix
+
./services/misc/anki-sync-server.nix
./services/misc/apache-kafka.nix
./services/misc/atuin.nix
./services/misc/autofs.nix
+68
nixos/modules/services/misc/anki-sync-server.md
···
+
# Anki Sync Server {#module-services-anki-sync-server}
+
+
[Anki Sync Server](https://docs.ankiweb.net/sync-server.html) is the built-in
+
sync server, present in recent versions of Anki. Advanced users who cannot or
+
do not wish to use AnkiWeb can use this sync server instead of AnkiWeb.
+
+
This module is compatible only with Anki versions >=2.1.66, due to [recent
+
enhancements to the Nix anki
+
package](https://github.com/NixOS/nixpkgs/commit/05727304f8815825565c944d012f20a9a096838a).
+
+
## Basic Usage {#module-services-anki-sync-server-basic-usage}
+
+
By default, the module creates a
+
[`systemd`](https://www.freedesktop.org/wiki/Software/systemd/)
+
unit which runs the sync server with an isolated user using the systemd
+
`DynamicUser` option.
+
+
This can be done by enabling the `anki-sync-server` service:
+
```
+
{ ... }:
+
+
{
+
services.anki-sync-server.enable = true;
+
}
+
```
+
+
It is necessary to set at least one username-password pair under
+
{option}`services.anki-sync-server.users`. For example
+
+
```
+
{
+
services.anki-sync-server.users = [
+
{
+
username = "user";
+
passwordFile = /etc/anki-sync-server/user;
+
}
+
];
+
}
+
```
+
+
Here, `passwordFile` is the path to a file containing just the password in
+
plaintext. Make sure to set permissions to make this file unreadable to any
+
user besides root.
+
+
By default, the server listen address {option}`services.anki-sync-server.host`
+
is set to localhost, listening on port
+
{option}`services.anki-sync-server.port`, and does not open the firewall. This
+
is suitable for purely local testing, or to be used behind a reverse proxy. If
+
you want to expose the sync server directly to other computers (not recommended
+
in most circumstances, because the sync server doesn't use HTTPS), then set the
+
following options:
+
+
```
+
{
+
services.anki-sync-server.host = "0.0.0.0";
+
services.anki-sync-server.openFirewall = true;
+
}
+
```
+
+
+
## Alternatives {#module-services-anki-sync-server-alternatives}
+
+
The [`ankisyncd` NixOS
+
module](https://github.com/NixOS/nixpkgs/blob/master/nixos/modules/services/misc/ankisyncd.nix)
+
provides similar functionality, but using a third-party implementation,
+
[`anki-sync-server-rs`](https://github.com/ankicommunity/anki-sync-server-rs/).
+
According to that project's README, it is "no longer maintained", and not
+
recommended for Anki 2.1.64+.
+146
nixos/modules/services/misc/anki-sync-server.nix
···
+
{
+
config,
+
lib,
+
pkgs,
+
utils,
+
...
+
}:
+
with lib; let
+
cfg = config.services.anki-sync-server;
+
name = "anki-sync-server";
+
specEscape = replaceStrings ["%"] ["%%"];
+
usersWithIndexes =
+
lists.imap1 (i: user: {
+
i = i;
+
user = user;
+
})
+
cfg.users;
+
usersWithIndexesFile = filter (x: x.user.passwordFile != null) usersWithIndexes;
+
usersWithIndexesNoFile = filter (x: x.user.passwordFile == null && x.user.password != null) usersWithIndexes;
+
anki-sync-server-run = pkgs.writeShellScriptBin "anki-sync-server-run" ''
+
# When services.anki-sync-server.users.passwordFile is set,
+
# each password file is passed as a systemd credential, which is mounted in
+
# a file system exposed to the service. Here we read the passwords from
+
# the credential files to pass them as environment variables to the Anki
+
# sync server.
+
${
+
concatMapStringsSep
+
"\n"
+
(x: ''export SYNC_USER${toString x.i}=${escapeShellArg x.user.username}:"''$(cat "''${CREDENTIALS_DIRECTORY}/"${escapeShellArg x.user.username})"'')
+
usersWithIndexesFile
+
}
+
# For users where services.anki-sync-server.users.password isn't set,
+
# export passwords in environment variables in plaintext.
+
${
+
concatMapStringsSep
+
"\n"
+
(x: ''export SYNC_USER${toString x.i}=${escapeShellArg x.user.username}:${escapeShellArg x.user.password}'')
+
usersWithIndexesNoFile
+
}
+
exec ${cfg.package}/bin/anki-sync-server
+
'';
+
in {
+
options.services.anki-sync-server = {
+
enable = mkEnableOption (lib.mdDoc "anki-sync-server");
+
+
package = mkOption {
+
type = types.package;
+
default = pkgs.anki-sync-server;
+
defaultText = literalExpression "pkgs.anki-sync-server";
+
description = lib.mdDoc "The package to use for the anki-sync-server command.";
+
};
+
+
address = mkOption {
+
type = types.str;
+
default = "::1";
+
description = lib.mdDoc ''
+
IP address anki-sync-server listens to.
+
Note host names are not resolved.
+
'';
+
};
+
+
port = mkOption {
+
type = types.port;
+
default = 27701;
+
description = lib.mdDoc "port anki-sync-server listens to";
+
};
+
+
openFirewall = mkOption {
+
default = false;
+
type = types.bool;
+
description = lib.mdDoc "Whether to open the firewall for the specified port.";
+
};
+
+
users = mkOption {
+
type = with types;
+
listOf (submodule {
+
options = {
+
username = mkOption {
+
type = str;
+
description = lib.mdDoc "User name accepted by anki-sync-server.";
+
};
+
password = mkOption {
+
type = nullOr str;
+
default = null;
+
description = lib.mdDoc ''
+
Password accepted by anki-sync-server for the associated username.
+
**WARNING**: This option is **not secure**. This password will
+
be stored in *plaintext* and will be visible to *all users*.
+
See {option}`services.anki-sync-server.users.passwordFile` for
+
a more secure option.
+
'';
+
};
+
passwordFile = mkOption {
+
type = nullOr path;
+
default = null;
+
description = lib.mdDoc ''
+
File containing the password accepted by anki-sync-server for
+
the associated username. Make sure to make readable only by
+
root.
+
'';
+
};
+
};
+
});
+
description = lib.mdDoc "List of user-password pairs to provide to the sync server.";
+
};
+
};
+
+
config = mkIf cfg.enable {
+
assertions = [
+
{
+
assertion = (builtins.length usersWithIndexesFile) + (builtins.length usersWithIndexesNoFile) > 0;
+
message = "At least one username-password pair must be set.";
+
}
+
];
+
networking.firewall.allowedTCPPorts = mkIf cfg.openFirewall [cfg.port];
+
+
systemd.services.anki-sync-server = {
+
description = "anki-sync-server: Anki sync server built into Anki";
+
after = ["network.target"];
+
wantedBy = ["multi-user.target"];
+
path = [cfg.package];
+
environment = {
+
SYNC_BASE = "%S/%N";
+
SYNC_HOST = specEscape cfg.address;
+
SYNC_PORT = toString cfg.port;
+
};
+
+
serviceConfig = {
+
Type = "simple";
+
DynamicUser = true;
+
StateDirectory = name;
+
ExecStart = "${anki-sync-server-run}/bin/anki-sync-server-run";
+
Restart = "always";
+
LoadCredential =
+
map
+
(x: "${specEscape x.user.username}:${specEscape (toString x.user.passwordFile)}")
+
usersWithIndexesFile;
+
};
+
};
+
};
+
+
meta = {
+
maintainers = with maintainers; [telotortium];
+
doc = ./anki-sync-server.md;
+
};
+
}