at master 7.9 kB view raw
1{ 2 config, 3 lib, 4 pkgs, 5 ... 6}: 7let 8 cfg = config.services.cross-seed; 9 10 inherit (lib) 11 mkEnableOption 12 mkPackageOption 13 mkOption 14 types 15 ; 16 settingsFormat = pkgs.formats.json { }; 17 18 generatedConfig = 19 pkgs.runCommand "cross-seed-gen-config" { nativeBuildInputs = [ pkgs.cross-seed ]; } 20 '' 21 export HOME=$(mktemp -d) 22 cross-seed gen-config 23 mkdir $out 24 cp -r $HOME/.cross-seed/config.js $out/ 25 ''; 26in 27{ 28 options.services.cross-seed = { 29 enable = mkEnableOption "cross-seed"; 30 31 package = mkPackageOption pkgs "cross-seed" { }; 32 33 user = mkOption { 34 type = types.str; 35 default = "cross-seed"; 36 description = "User to run cross-seed as."; 37 }; 38 39 group = mkOption { 40 type = types.str; 41 default = "cross-seed"; 42 example = "torrents"; 43 description = "Group to run cross-seed as."; 44 }; 45 46 configDir = mkOption { 47 type = types.path; 48 default = "/var/lib/cross-seed"; 49 description = "Cross-seed config directory"; 50 }; 51 52 useGenConfigDefaults = mkOption { 53 type = types.bool; 54 default = false; 55 description = '' 56 Whether to use the option defaults from the configuration generated by 57 {command}`cross-seed gen-config`. 58 59 Those are the settings recommended by the project, and can be inspected 60 from their [template file](https://github.com/cross-seed/cross-seed/blob/master/src/config.template.cjs). 61 62 Settings set in {option}`services.cross-seed.settings` and 63 {option}`services.cross-seed.settingsFile` will override the ones from 64 this option. 65 ''; 66 }; 67 68 settings = mkOption { 69 default = { }; 70 type = types.submodule { 71 freeformType = settingsFormat.type; 72 options = { 73 dataDirs = mkOption { 74 type = types.listOf types.path; 75 default = [ ]; 76 description = '' 77 Paths to be searched for matching data. 78 79 If you use Injection, cross-seed will use the specified linkType 80 to create a link to the original file in the linkDirs. 81 82 If linkType is hardlink, these must be on the same volume as the 83 data. 84 ''; 85 }; 86 87 linkDirs = mkOption { 88 type = types.listOf types.path; 89 default = [ ]; 90 description = '' 91 List of directories where cross-seed will create links. 92 93 If linkType is hardlink, these must be on the same volume as the data. 94 ''; 95 }; 96 97 torrentDir = mkOption { 98 type = types.nullOr types.path; 99 default = null; 100 description = '' 101 Directory containing torrent files, or if you're using a torrent 102 client integration and injection - your torrent client's .torrent 103 file store/cache. 104 ''; 105 }; 106 107 outputDir = mkOption { 108 type = types.path; 109 default = "${cfg.configDir}/output"; 110 defaultText = ''''${cfg.configDir}/output''; 111 description = "Directory where cross-seed will place torrent files it finds."; 112 }; 113 114 port = mkOption { 115 type = types.port; 116 default = 2468; 117 example = 3000; 118 description = "Port the cross-seed daemon listens on."; 119 }; 120 }; 121 }; 122 123 description = '' 124 Configuration options for cross-seed. 125 126 Secrets should not be set in this option, as they will be available in 127 the Nix store. For secrets, please use settingsFile. 128 129 For more details, see [the cross-seed documentation](https://www.cross-seed.org/docs/basics/options). 130 ''; 131 }; 132 133 settingsFile = lib.mkOption { 134 default = null; 135 type = types.nullOr types.path; 136 description = '' 137 Path to a JSON file containing settings that will be merged with the 138 settings option. This is suitable for storing secrets, as they will not 139 be exposed on the Nix store. 140 ''; 141 }; 142 }; 143 144 config = 145 let 146 jsonSettingsFile = settingsFormat.generate "settings.json" cfg.settings; 147 148 genConfigSegment = 149 lib.optionalString cfg.useGenConfigDefaults # js 150 '' 151 const gen_config_js = "${generatedConfig}/config.js"; 152 Object.assign(loaded_settings, require(gen_config_js)); 153 ''; 154 155 # Since cross-seed uses a javascript config file, we can use node's 156 # ability to parse JSON directly to avoid having to do any conversion. 157 # This also means we don't need to use any external programs to merge the 158 # secrets. 159 secretSettingsSegment = 160 lib.optionalString (cfg.settingsFile != null) # js 161 '' 162 const path = require("node:path"); 163 const secret_settings_json = path.join(process.env.CREDENTIALS_DIRECTORY, "secretSettingsFile"); 164 Object.assign(loaded_settings, JSON.parse(fs.readFileSync(secret_settings_json, "utf8"))); 165 ''; 166 167 javascriptConfig = 168 pkgs.writeText "config.js" # js 169 '' 170 "use strict"; 171 const fs = require("fs"); 172 const settings_json = "${jsonSettingsFile}"; 173 let loaded_settings = {}; 174 ${genConfigSegment} 175 Object.assign(loaded_settings, JSON.parse(fs.readFileSync(settings_json, "utf8"))); 176 ${secretSettingsSegment} 177 module.exports = loaded_settings; 178 ''; 179 in 180 lib.mkIf (cfg.enable) { 181 assertions = [ 182 { 183 assertion = !(cfg.settings ? apiKey); 184 message = '' 185 The API key should be set via the settingsFile option, to avoid 186 exposing it on the Nix store. 187 ''; 188 } 189 ]; 190 191 systemd.tmpfiles.settings."10-cross-seed" = { 192 ${cfg.configDir}.d = { 193 inherit (cfg) group user; 194 mode = "700"; 195 }; 196 197 ${cfg.settings.outputDir}.d = { 198 inherit (cfg) group user; 199 mode = "750"; 200 }; 201 }; 202 203 systemd.services.cross-seed = { 204 description = "cross-seed"; 205 after = [ "network-online.target" ]; 206 wants = [ "network-online.target" ]; 207 wantedBy = [ "multi-user.target" ]; 208 environment.CONFIG_DIR = cfg.configDir; 209 preStart = '' 210 install -D -m 600 -o '${cfg.user}' -g '${cfg.group}' '${javascriptConfig}' '${cfg.configDir}/config.js' 211 ''; 212 213 serviceConfig = { 214 ExecStart = "${lib.getExe cfg.package} daemon"; 215 User = cfg.user; 216 Group = cfg.group; 217 218 # Only allow binding to the specified port. 219 SocketBindDeny = "any"; 220 SocketBindAllow = cfg.settings.port; 221 222 LoadCredential = lib.mkIf (cfg.settingsFile != null) "secretSettingsFile:${cfg.settingsFile}"; 223 224 StateDirectory = "cross-seed"; 225 ReadWritePaths = [ cfg.settings.outputDir ]; 226 ReadOnlyPaths = lib.optional (cfg.settings.torrentDir != null) cfg.settings.torrentDir; 227 }; 228 229 unitConfig = { 230 # 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. 231 RequiresMountsFor = lib.flatten [ 232 cfg.settings.dataDirs 233 cfg.settings.linkDirs 234 cfg.settings.outputDir 235 ]; 236 }; 237 }; 238 239 # It's useful to have the package in the path, to be able to e.g. get the API key. 240 environment.systemPackages = [ cfg.package ]; 241 242 users.users = lib.mkIf (cfg.user == "cross-seed") { 243 cross-seed = { 244 group = cfg.group; 245 description = "cross-seed user"; 246 isSystemUser = true; 247 home = cfg.configDir; 248 }; 249 }; 250 251 users.groups = lib.mkIf (cfg.group == "cross-seed") { 252 cross-seed = { }; 253 }; 254 }; 255}