-
-
Notifications
You must be signed in to change notification settings - Fork 18.8k
nixos/anki-sync-server: init #257692
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
nixos/anki-sync-server: init #257692
Changes from all commits
Commits
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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+. | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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; | ||
| }; | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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}") | ||
| ''; | ||
| }) |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.