nixos/cross-seed: init module

This commit is contained in:
Pedro Alves 2025-02-14 13:48:04 +00:00
parent e825e5bc2a
commit 3b3bced9c5
6 changed files with 264 additions and 0 deletions

View file

@ -111,6 +111,8 @@
- [autobrr](https://autobrr.com), a modern download automation tool for torrents and usenets. Available as [services.autobrr](#opt-services.autobrr.enable). - [autobrr](https://autobrr.com), a modern download automation tool for torrents and usenets. Available as [services.autobrr](#opt-services.autobrr.enable).
- [cross-seed](https://www.cross-seed.org), a tool to set-up fully automatic cross-seeding of torrents. Available as [services.cross-seed](#opt-services.cross-seed.enable).
- [agorakit](https://github.com/agorakit/agorakit), an organization tool for citizens' collectives. Available with [services.agorakit](options.html#opt-services.agorakit.enable). - [agorakit](https://github.com/agorakit/agorakit), an organization tool for citizens' collectives. Available with [services.agorakit](options.html#opt-services.agorakit.enable).
- [vivid](https://github.com/sharkdp/vivid), a generator for LS_COLOR. Available as [programs.vivid](#opt-programs.vivid.enable). - [vivid](https://github.com/sharkdp/vivid), a generator for LS_COLOR. Available as [programs.vivid](#opt-programs.vivid.enable).

View file

@ -1424,6 +1424,7 @@
./services/system/userborn.nix ./services/system/userborn.nix
./services/system/zram-generator.nix ./services/system/zram-generator.nix
./services/torrent/bitmagnet.nix ./services/torrent/bitmagnet.nix
./services/torrent/cross-seed.nix
./services/torrent/deluge.nix ./services/torrent/deluge.nix
./services/torrent/flexget.nix ./services/torrent/flexget.nix
./services/torrent/flood.nix ./services/torrent/flood.nix

View file

@ -0,0 +1,214 @@
{
config,
lib,
pkgs,
...
}:
let
cfg = config.services.cross-seed;
inherit (lib)
mkEnableOption
mkPackageOption
mkOption
types
;
settingsFormat = pkgs.formats.json { };
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";
};
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;
# 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 = 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";
};
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 = { };
};
};
}

View file

@ -253,6 +253,7 @@ in {
curl-impersonate = handleTest ./curl-impersonate.nix {}; curl-impersonate = handleTest ./curl-impersonate.nix {};
custom-ca = handleTest ./custom-ca.nix {}; custom-ca = handleTest ./custom-ca.nix {};
croc = handleTest ./croc.nix {}; croc = handleTest ./croc.nix {};
cross-seed = runTest ./cross-seed.nix;
cyrus-imap = runTest ./cyrus-imap.nix; cyrus-imap = runTest ./cyrus-imap.nix;
darling = handleTest ./darling.nix {}; darling = handleTest ./darling.nix {};
darling-dmg = runTest ./darling-dmg.nix; darling-dmg = runTest ./darling-dmg.nix;

View file

@ -0,0 +1,43 @@
{ lib, ... }:
let
apiKey = "twentyfourcharacterskey!";
in
{
name = "cross-seed";
meta.maintainers = with lib.maintainers; [ pta2002 ];
nodes.machine =
{ pkgs, config, ... }:
let
cfg = config.services.cross-seed;
in
{
systemd.tmpfiles.settings."0-cross-seed-test"."${cfg.settings.torrentDir}".d = {
inherit (cfg) user group;
mode = "700";
};
services.cross-seed = {
enable = true;
settings = {
outputDir = "/var/lib/cross-seed/output";
torrentDir = "/var/lib/torrents";
torznab = [ ];
useClientTorrents = false;
};
# # We create this secret in the Nix store (making it readable by everyone).
# # DO NOT DO THIS OUTSIDE OF TESTS!!
settingsFile = (pkgs.formats.json { }).generate "secrets.json" {
inherit apiKey;
};
};
};
testScript = # python
''
start_all()
machine.wait_for_unit("cross-seed.service")
machine.wait_for_open_port(2468)
machine.succeed("curl --fail -XPOST http://localhost:2468/api/search?apiKey=${apiKey}")
'';
}

View file

@ -2,6 +2,7 @@
lib, lib,
buildNpmPackage, buildNpmPackage,
fetchFromGitHub, fetchFromGitHub,
nixosTests,
}: }:
buildNpmPackage rec { buildNpmPackage rec {
@ -17,6 +18,8 @@ buildNpmPackage rec {
npmDepsHash = "sha256-hqQi0kSPm9SKEoLu6InvRMPxbQ+CBpKVPJhhOdo2ZII="; npmDepsHash = "sha256-hqQi0kSPm9SKEoLu6InvRMPxbQ+CBpKVPJhhOdo2ZII=";
passthru.tests.cross-seed = nixosTests.cross-seed;
meta = { meta = {
description = "Fully-automatic torrent cross-seeding with Torznab"; description = "Fully-automatic torrent cross-seeding with Torznab";
homepage = "https://cross-seed.org"; homepage = "https://cross-seed.org";