Merge pull request #257692 from telotortium/anki-sync-server
nixos/anki-sync-server: init
This commit is contained in:
commit
07183914f1
|
@ -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).
|
- [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}
|
## Backward Incompatibilities {#sec-release-24.05-incompatibilities}
|
||||||
|
|
||||||
<!-- To avoid merge conflicts, consider adding your item at an arbitrary place in the list instead. -->
|
<!-- To avoid merge conflicts, consider adding your item at an arbitrary place in the list instead. -->
|
||||||
|
|
|
@ -637,6 +637,7 @@
|
||||||
./services/misc/amazon-ssm-agent.nix
|
./services/misc/amazon-ssm-agent.nix
|
||||||
./services/misc/ananicy.nix
|
./services/misc/ananicy.nix
|
||||||
./services/misc/ankisyncd.nix
|
./services/misc/ankisyncd.nix
|
||||||
|
./services/misc/anki-sync-server.nix
|
||||||
./services/misc/apache-kafka.nix
|
./services/misc/apache-kafka.nix
|
||||||
./services/misc/atuin.nix
|
./services/misc/atuin.nix
|
||||||
./services/misc/autofs.nix
|
./services/misc/autofs.nix
|
||||||
|
|
68
nixos/modules/services/misc/anki-sync-server.md
Normal file
68
nixos/modules/services/misc/anki-sync-server.md
Normal file
|
@ -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+.
|
140
nixos/modules/services/misc/anki-sync-server.nix
Normal file
140
nixos/modules/services/misc/anki-sync-server.nix
Normal file
|
@ -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;
|
||||||
|
};
|
||||||
|
}
|
|
@ -120,6 +120,7 @@ in {
|
||||||
amazon-ssm-agent = handleTest ./amazon-ssm-agent.nix {};
|
amazon-ssm-agent = handleTest ./amazon-ssm-agent.nix {};
|
||||||
amd-sev = runTest ./amd-sev.nix;
|
amd-sev = runTest ./amd-sev.nix;
|
||||||
anbox = runTest ./anbox.nix;
|
anbox = runTest ./anbox.nix;
|
||||||
|
anki-sync-server = handleTest ./anki-sync-server.nix {};
|
||||||
anuko-time-tracker = handleTest ./anuko-time-tracker.nix {};
|
anuko-time-tracker = handleTest ./anuko-time-tracker.nix {};
|
||||||
apcupsd = handleTest ./apcupsd.nix {};
|
apcupsd = handleTest ./apcupsd.nix {};
|
||||||
apfs = runTest ./apfs.nix;
|
apfs = runTest ./apfs.nix;
|
||||||
|
|
71
nixos/tests/anki-sync-server.nix
Normal file
71
nixos/tests/anki-sync-server.nix
Normal file
|
@ -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}")
|
||||||
|
'';
|
||||||
|
})
|
|
@ -9,6 +9,7 @@
|
||||||
, lame
|
, lame
|
||||||
, mpv-unwrapped
|
, mpv-unwrapped
|
||||||
, ninja
|
, ninja
|
||||||
|
, nixosTests
|
||||||
, nodejs
|
, nodejs
|
||||||
, nodejs-slim
|
, nodejs-slim
|
||||||
, prefetch-yarn-deps
|
, prefetch-yarn-deps
|
||||||
|
@ -270,6 +271,7 @@ python3.pkgs.buildPythonApplication {
|
||||||
passthru = {
|
passthru = {
|
||||||
# cargoLock is reused in anki-sync-server
|
# cargoLock is reused in anki-sync-server
|
||||||
inherit cargoLock;
|
inherit cargoLock;
|
||||||
|
tests.anki-sync-server = nixosTests.anki-sync-server;
|
||||||
};
|
};
|
||||||
|
|
||||||
meta = with lib; {
|
meta = with lib; {
|
||||||
|
|
Loading…
Reference in New Issue
Block a user