nixpkgs/nixos/modules/services/torrent/cross-seed.nix
Bruno BELANYI 9cc26fef84 nixos/cross-seed: add 'useGenConfigDefaults'
This to me is the cleanest way of setting the default value and track
changes to the values from `cross-seed gen-config` with low churn.
2025-04-14 15:49:27 +00:00

256 lines
7.7 KiB
Nix

{
config,
lib,
pkgs,
...
}:
let
cfg = config.services.cross-seed;
inherit (lib)
mkEnableOption
mkPackageOption
mkOption
types
;
settingsFormat = pkgs.formats.json { };
generatedConfig =
pkgs.runCommand "cross-seed-gen-config" { nativeBuildInputs = [ pkgs.cross-seed ]; }
''
export HOME=$(mktemp -d)
cross-seed gen-config
mkdir $out
cp -r $HOME/.cross-seed/config.js $out/
'';
in
{
options.services.cross-seed = {
enable = mkEnableOption "cross-seed";
package = mkPackageOption pkgs "cross-seed" { };
user = mkOption {
type = types.str;
default = "cross-seed";
description = "User to run cross-seed as.";
};
group = mkOption {
type = types.str;
default = "cross-seed";
example = "torrents";
description = "Group to run cross-seed as.";
};
configDir = mkOption {
type = types.path;
default = "/var/lib/cross-seed";
description = "Cross-seed config directory";
};
useGenConfigDefaults = mkOption {
type = types.bool;
default = false;
description = ''
Whether to use the option defaults from the configuration generated by
{command}`cross-seed gen-config`.
Those are the settings recommended by the project, and can be inspected
from their [template file](https://github.com/cross-seed/cross-seed/blob/master/src/config.template.cjs).
Settings set in {option}`services.cross-seed.settings` and
{option}`services.cross-seed.settingsFile` will override the ones from
this option.
'';
};
settings = mkOption {
default = { };
type = types.submodule {
freeformType = settingsFormat.type;
options = {
dataDirs = mkOption {
type = types.listOf types.path;
default = [ ];
description = ''
Paths to be searched for matching data.
If you use Injection, cross-seed will use the specified linkType
to create a link to the original file in the linkDirs.
If linkType is hardlink, these must be on the same volume as the
data.
'';
};
linkDirs = mkOption {
type = types.listOf types.path;
default = [ ];
description = ''
List of directories where cross-seed will create links.
If linkType is hardlink, these must be on the same volume as the data.
'';
};
torrentDir = mkOption {
type = types.nullOr types.path;
default = null;
description = ''
Directory containing torrent files, or if you're using a torrent
client integration and injection - your torrent client's .torrent
file store/cache.
'';
};
outputDir = mkOption {
type = types.path;
default = "${cfg.configDir}/output";
defaultText = ''''${cfg.configDir}/output'';
description = "Directory where cross-seed will place torrent files it finds.";
};
port = mkOption {
type = types.port;
default = 2468;
example = 3000;
description = "Port the cross-seed daemon listens on.";
};
};
};
description = ''
Configuration options for cross-seed.
Secrets should not be set in this option, as they will be available in
the Nix store. For secrets, please use settingsFile.
For more details, see [the cross-seed documentation](https://www.cross-seed.org/docs/basics/options).
'';
};
settingsFile = lib.mkOption {
default = null;
type = types.nullOr types.path;
description = ''
Path to a JSON file containing settings that will be merged with the
settings option. This is suitable for storing secrets, as they will not
be exposed on the Nix store.
'';
};
};
config =
let
jsonSettingsFile = settingsFormat.generate "settings.json" cfg.settings;
genConfigSegment =
lib.optionalString cfg.useGenConfigDefaults # js
''
const gen_config_js = "${generatedConfig}/config.js";
Object.assign(loaded_settings, require(gen_config_js));
'';
# Since cross-seed uses a javascript config file, we can use node's
# ability to parse JSON directly to avoid having to do any conversion.
# This also means we don't need to use any external programs to merge the
# secrets.
secretSettingsSegment =
lib.optionalString (cfg.settingsFile != null) # js
''
const path = require("node:path");
const secret_settings_json = path.join(process.env.CREDENTIALS_DIRECTORY, "secretSettingsFile");
Object.assign(loaded_settings, JSON.parse(fs.readFileSync(secret_settings_json, "utf8")));
'';
javascriptConfig =
pkgs.writeText "config.js" # js
''
"use strict";
const fs = require("fs");
const settings_json = "${jsonSettingsFile}";
let loaded_settings = {};
${genConfigSegment}
Object.assign(loaded_settings, JSON.parse(fs.readFileSync(settings_json, "utf8")));
${secretSettingsSegment}
module.exports = loaded_settings;
'';
in
lib.mkIf (cfg.enable) {
assertions = [
{
assertion = !(cfg.settings ? apiKey);
message = ''
The API key should be set via the settingsFile option, to avoid
exposing it on the Nix store.
'';
}
];
systemd.tmpfiles.settings."10-cross-seed" = {
${cfg.configDir}.d = {
inherit (cfg) group user;
mode = "700";
};
${cfg.settings.outputDir}.d = {
inherit (cfg) group user;
mode = "750";
};
};
systemd.services.cross-seed = {
description = "cross-seed";
after = [ "network-online.target" ];
wants = [ "network-online.target" ];
wantedBy = [ "multi-user.target" ];
environment.CONFIG_DIR = cfg.configDir;
preStart = ''
install -D -m 600 -o '${cfg.user}' -g '${cfg.group}' '${javascriptConfig}' '${cfg.configDir}/config.js'
'';
serviceConfig = {
ExecStart = "${lib.getExe cfg.package} daemon";
User = cfg.user;
Group = cfg.group;
# Only allow binding to the specified port.
SocketBindDeny = "any";
SocketBindAllow = cfg.settings.port;
LoadCredential = lib.mkIf (cfg.settingsFile != null) "secretSettingsFile:${cfg.settingsFile}";
StateDirectory = "cross-seed";
ReadWritePaths = [ cfg.settings.outputDir ];
ReadOnlyPaths = lib.optional (cfg.settings.torrentDir != null) cfg.settings.torrentDir;
};
unitConfig = {
# Unfortunately, we can not protect these if we are to hardlink between them, as they need to be on the same volume for hardlinks to work.
RequiresMountsFor = lib.flatten [
cfg.settings.dataDirs
cfg.settings.linkDirs
cfg.settings.outputDir
];
};
};
# It's useful to have the package in the path, to be able to e.g. get the API key.
environment.systemPackages = [ cfg.package ];
users.users = lib.mkIf (cfg.user == "cross-seed") {
cross-seed = {
group = cfg.group;
description = "cross-seed user";
isSystemUser = true;
home = cfg.configDir;
};
};
users.groups = lib.mkIf (cfg.group == "cross-seed") {
cross-seed = { };
};
};
}