2023-11-27 21:55:35 +00: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;
|
|
|
|
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 = {
|
2023-11-30 16:32:03 +00:00
|
|
|
enable = mkEnableOption "anki-sync-server";
|
2023-11-27 21:55:35 +00:00
|
|
|
|
2023-11-30 16:32:03 +00:00
|
|
|
package = mkPackageOption pkgs "anki-sync-server" { };
|
2023-11-27 21:55:35 +00:00
|
|
|
|
|
|
|
address = mkOption {
|
|
|
|
type = types.str;
|
|
|
|
default = "::1";
|
2023-11-30 16:32:03 +00:00
|
|
|
description = ''
|
2023-11-27 21:55:35 +00:00
|
|
|
IP address anki-sync-server listens to.
|
|
|
|
Note host names are not resolved.
|
|
|
|
'';
|
|
|
|
};
|
|
|
|
|
|
|
|
port = mkOption {
|
|
|
|
type = types.port;
|
|
|
|
default = 27701;
|
2023-11-30 16:32:03 +00:00
|
|
|
description = "Port number anki-sync-server listens to.";
|
2023-11-27 21:55:35 +00:00
|
|
|
};
|
|
|
|
|
|
|
|
openFirewall = mkOption {
|
|
|
|
default = false;
|
|
|
|
type = types.bool;
|
2023-11-30 16:32:03 +00:00
|
|
|
description = "Whether to open the firewall for the specified port.";
|
2023-11-27 21:55:35 +00:00
|
|
|
};
|
|
|
|
|
|
|
|
users = mkOption {
|
|
|
|
type = with types;
|
|
|
|
listOf (submodule {
|
|
|
|
options = {
|
|
|
|
username = mkOption {
|
|
|
|
type = str;
|
2023-11-30 16:32:03 +00:00
|
|
|
description = "User name accepted by anki-sync-server.";
|
2023-11-27 21:55:35 +00:00
|
|
|
};
|
|
|
|
password = mkOption {
|
|
|
|
type = nullOr str;
|
|
|
|
default = null;
|
2023-11-30 16:32:03 +00:00
|
|
|
description = ''
|
2023-11-27 21:55:35 +00: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 16:32:03 +00:00
|
|
|
description = ''
|
2023-11-27 21:55:35 +00: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 16:32:03 +00:00
|
|
|
description = "List of user-password pairs to provide to the sync server.";
|
2023-11-27 21:55:35 +00: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 = {
|
|
|
|
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;
|
|
|
|
};
|
|
|
|
}
|