2023-11-27 13:55:35 -08:00
|
|
|
{
|
|
|
|
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;
|
2024-01-13 21:14:28 +09:00
|
|
|
anki-sync-server-run = pkgs.writeShellScript "anki-sync-server-run" ''
|
2023-11-27 13:55:35 -08:00
|
|
|
# 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: ''
|
2024-01-13 21:14:28 +09:00
|
|
|
read -r pass < "''${CREDENTIALS_DIRECTORY}/"${escapeShellArg x.user.username}
|
|
|
|
export SYNC_USER${toString x.i}=${escapeShellArg x.user.username}:"$pass"
|
2023-11-27 13:55:35 -08:00
|
|
|
'') 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}
|
2024-01-13 21:14:28 +09:00
|
|
|
exec ${lib.getExe cfg.package}
|
2023-11-27 13:55:35 -08:00
|
|
|
'';
|
|
|
|
in
|
|
|
|
{
|
|
|
|
options.services.anki-sync-server = {
|
2023-11-30 17:32:03 +01:00
|
|
|
enable = mkEnableOption "anki-sync-server";
|
2023-11-27 13:55:35 -08:00
|
|
|
|
2023-11-30 17:32:03 +01:00
|
|
|
package = mkPackageOption pkgs "anki-sync-server" { };
|
2023-11-27 13:55:35 -08:00
|
|
|
|
|
|
|
address = mkOption {
|
|
|
|
type = types.str;
|
|
|
|
default = "::1";
|
2023-11-30 17:32:03 +01:00
|
|
|
description = ''
|
2023-11-27 13:55:35 -08:00
|
|
|
IP address anki-sync-server listens to.
|
|
|
|
Note host names are not resolved.
|
|
|
|
'';
|
|
|
|
};
|
|
|
|
|
|
|
|
port = mkOption {
|
|
|
|
type = types.port;
|
|
|
|
default = 27701;
|
2023-11-30 17:32:03 +01:00
|
|
|
description = "Port number anki-sync-server listens to.";
|
2023-11-27 13:55:35 -08:00
|
|
|
};
|
|
|
|
|
2024-09-26 22:13:28 +02:00
|
|
|
baseDirectory = mkOption {
|
|
|
|
type = types.str;
|
|
|
|
default = "%S/%N";
|
|
|
|
description = "Base directory where user(s) synchronized data will be stored.";
|
|
|
|
};
|
|
|
|
|
2023-11-27 13:55:35 -08:00
|
|
|
openFirewall = mkOption {
|
|
|
|
default = false;
|
|
|
|
type = types.bool;
|
2023-11-30 17:32:03 +01:00
|
|
|
description = "Whether to open the firewall for the specified port.";
|
2023-11-27 13:55:35 -08:00
|
|
|
};
|
|
|
|
|
|
|
|
users = mkOption {
|
|
|
|
type =
|
|
|
|
with types;
|
|
|
|
listOf (submodule {
|
|
|
|
options = {
|
|
|
|
username = mkOption {
|
|
|
|
type = str;
|
2023-11-30 17:32:03 +01:00
|
|
|
description = "User name accepted by anki-sync-server.";
|
2023-11-27 13:55:35 -08:00
|
|
|
};
|
|
|
|
password = mkOption {
|
|
|
|
type = nullOr str;
|
|
|
|
default = null;
|
2023-11-30 17:32:03 +01:00
|
|
|
description = ''
|
2023-11-27 13:55:35 -08:00
|
|
|
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;
|
2023-11-30 17:32:03 +01:00
|
|
|
description = ''
|
2023-11-27 13:55:35 -08:00
|
|
|
File containing the password accepted by anki-sync-server for
|
|
|
|
the associated username. Make sure to make readable only by
|
|
|
|
root.
|
|
|
|
'';
|
|
|
|
};
|
|
|
|
};
|
|
|
|
});
|
2023-11-30 17:32:03 +01:00
|
|
|
description = "List of user-password pairs to provide to the sync server.";
|
2023-11-27 13:55:35 -08:00
|
|
|
};
|
|
|
|
};
|
|
|
|
|
|
|
|
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 = {
|
2024-09-26 22:13:28 +02:00
|
|
|
SYNC_BASE = cfg.baseDirectory;
|
2023-11-27 13:55:35 -08:00
|
|
|
SYNC_HOST = specEscape cfg.address;
|
|
|
|
SYNC_PORT = toString cfg.port;
|
|
|
|
};
|
|
|
|
|
|
|
|
serviceConfig = {
|
|
|
|
Type = "simple";
|
|
|
|
DynamicUser = true;
|
|
|
|
StateDirectory = name;
|
2024-01-13 21:14:28 +09:00
|
|
|
ExecStart = anki-sync-server-run;
|
2023-11-27 13:55:35 -08:00
|
|
|
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;
|
|
|
|
};
|
|
|
|
}
|