mirror of
https://github.com/SebastianWendel/nixpkgs.git
synced 2024-09-20 04:19:00 +02:00
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).
|
||||
|
||||
- [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. -->
|
||||
|
|
|
@ -637,6 +637,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
|
||||
|
|
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 {};
|
||||
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;
|
||||
|
|
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
|
||||
, mpv-unwrapped
|
||||
, ninja
|
||||
, nixosTests
|
||||
, nodejs
|
||||
, nodejs-slim
|
||||
, prefetch-yarn-deps
|
||||
|
@ -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; {
|
||||
|
|
Loading…
Reference in a new issue