diff --git a/nixos/doc/manual/release-notes/rl-2405.section.md b/nixos/doc/manual/release-notes/rl-2405.section.md index b6b343145d78..8599f54cf479 100644 --- a/nixos/doc/manual/release-notes/rl-2405.section.md +++ b/nixos/doc/manual/release-notes/rl-2405.section.md @@ -16,6 +16,8 @@ In addition to numerous new and upgraded packages, this release has the followin - [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} diff --git a/nixos/modules/module-list.nix b/nixos/modules/module-list.nix index e40b7ed8015f..87129de4ca34 100644 --- a/nixos/modules/module-list.nix +++ b/nixos/modules/module-list.nix @@ -635,6 +635,7 @@ ./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 diff --git a/nixos/modules/services/misc/anki-sync-server.md b/nixos/modules/services/misc/anki-sync-server.md new file mode 100644 index 000000000000..5d2b4da4d2fc --- /dev/null +++ b/nixos/modules/services/misc/anki-sync-server.md @@ -0,0 +1,68 @@ +# 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+. diff --git a/nixos/modules/services/misc/anki-sync-server.nix b/nixos/modules/services/misc/anki-sync-server.nix new file mode 100644 index 000000000000..d4805253834b --- /dev/null +++ b/nixos/modules/services/misc/anki-sync-server.nix @@ -0,0 +1,146 @@ +{ + 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; + }; +}