Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions nixos/doc/manual/release-notes/rl-2405.section.md
Original file line number Diff line number Diff line change
Expand Up @@ -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}

<!-- To avoid merge conflicts, consider adding your item at an arbitrary place in the list instead. -->
Expand Down
1 change: 1 addition & 0 deletions nixos/modules/module-list.nix
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
68 changes: 68 additions & 0 deletions nixos/modules/services/misc/anki-sync-server.md
Original file line number Diff line number Diff line change
@@ -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+.
Comment thread
telotortium marked this conversation as resolved.
Outdated
140 changes: 140 additions & 0 deletions nixos/modules/services/misc/anki-sync-server.nix
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
{
config,
lib,
pkgs,
...
}:
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 "anki-sync-server";

package = mkPackageOption pkgs "anki-sync-server" { };

address = mkOption {
type = types.str;
default = "::1";
description = ''
IP address anki-sync-server listens to.
Note host names are not resolved.
'';
};

port = mkOption {
type = types.port;
default = 27701;
description = "Port number anki-sync-server listens to.";
};

openFirewall = mkOption {
default = false;
type = types.bool;
description = "Whether to open the firewall for the specified port.";
};

users = mkOption {
type = with types;
listOf (submodule {
options = {
username = mkOption {
type = str;
description = "User name accepted by anki-sync-server.";
};
password = mkOption {
type = nullOr str;
default = null;
description = ''
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 = ''
File containing the password accepted by anki-sync-server for
the associated username. Make sure to make readable only by
root.
'';
};
};
});
description = "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;
};
}
1 change: 1 addition & 0 deletions nixos/tests/all-tests.nix
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,7 @@ in {
amazon-ssm-agent = handleTest ./amazon-ssm-agent.nix {};
amd-sev = runTest ./amd-sev.nix;
anbox = runTest ./anbox.nix;
anki-sync-server = handleTest ./anki-sync-server.nix {};
anuko-time-tracker = handleTest ./anuko-time-tracker.nix {};
apcupsd = handleTest ./apcupsd.nix {};
apfs = runTest ./apfs.nix;
Expand Down
71 changes: 71 additions & 0 deletions nixos/tests/anki-sync-server.nix
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
import ./make-test-python.nix ({ pkgs, ... }:
let
ankiSyncTest = pkgs.writeScript "anki-sync-test.py" ''
#!${pkgs.python3}/bin/python

import sys

# get site paths from anki itself
from runpy import run_path
run_path("${pkgs.anki}/bin/.anki-wrapped")
import anki

col = anki.collection.Collection('test_collection')
endpoint = 'http://localhost:27701'

# Sanity check: verify bad login fails
try:
col.sync_login('baduser', 'badpass', endpoint)
print("bad user login worked?!")
sys.exit(1)
except anki.errors.SyncError:
pass

# test logging in to users
col.sync_login('user', 'password', endpoint)
col.sync_login('passfileuser', 'passfilepassword', endpoint)

# Test actual sync. login apparently doesn't remember the endpoint...
login = col.sync_login('user', 'password', endpoint)
login.endpoint = endpoint
sync = col.sync_collection(login, False)
assert sync.required == sync.NO_CHANGES
# TODO: create an archive with server content including a test card
# and check we got it?
'';
testPasswordFile = pkgs.writeText "anki-password" "passfilepassword";
in
{
name = "anki-sync-server";
meta = with pkgs.lib.maintainers; {
maintainers = [ martinetd ];
};

nodes.machine = { pkgs, ...}: {
services.anki-sync-server = {
enable = true;
users = [
{ username = "user";
password = "password";
}
{ username = "passfileuser";
passwordFile = testPasswordFile;
}
];
};
};


testScript =
''
start_all()

with subtest("Server starts successfully"):
# service won't start without users
machine.wait_for_unit("anki-sync-server.service")
machine.wait_for_open_port(27701)

with subtest("Can sync"):
machine.succeed("${ankiSyncTest}")
'';
})
2 changes: 2 additions & 0 deletions pkgs/games/anki/default.nix
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
, lame
, mpv-unwrapped
, ninja
, nixosTests
, nodejs
, nodejs-slim
, prefetch-yarn-deps
Expand Down Expand Up @@ -270,6 +271,7 @@ python3.pkgs.buildPythonApplication {
passthru = {
# cargoLock is reused in anki-sync-server
inherit cargoLock;
tests.anki-sync-server = nixosTests.anki-sync-server;
};

meta = with lib; {
Expand Down