{ config, lib, pkgs, ... }: let cfg = config.services.copyparty; # NOLINT(let_order) writableVolumes = lib.filter (volume: !volume.readOnly) (lib.attrValues cfg.volumes); credentialName = name: "password-${builtins.substring 0 16 (builtins.hashString "sha256" name)}"; passwordPlaceholder = name: "@COPYPARTY_PASSWORD_${builtins.substring 0 16 (builtins.hashString "sha256" name)}@"; renderLine = indent: name: value: if value == true then "${indent}${name}" else if value == false then null else if lib.isList value then "${indent}${name}: ${lib.concatStringsSep ", " (map toString value)}" else "${indent}${name}: ${toString value}"; renderAttrs = indent: attrs: lib.pipe attrs [ lib.attrsToList (map ({ name, value }: renderLine indent name value)) (lib.filter (line: line != null)) (lib.concatStringsSep "\n") ]; renderSectionName = name: "[${lib.escape [ "[" "]" ] name}]"; renderSection = name: attrs: '' ${renderSectionName name} ${renderAttrs "" attrs} ''; renderSubsection = name: attrs: lib.optionalString (attrs != { }) '' ${name}: ${renderAttrs " " attrs} ''; renderVolume = urlPath: volume: '' ${renderSectionName urlPath} ${volume.path} ${renderSubsection "accs" volume.access} ${renderSubsection "flags" (volumeFlags volume.flags)} ''; volumeFlags = flags: lib.optionalAttrs (flags != { }) ( { chmod_d = "2770"; chmod_f = "660"; } // flags ); accountPlaceholders = lib.mapAttrs (name: _account: passwordPlaceholder name) cfg.accounts; configText = '' ${renderSection "global" cfg.settings} ${cfg.globalExtraConfig} ${renderSection "accounts" accountPlaceholders} ${lib.concatStringsSep "\n" (lib.mapAttrsToList renderVolume cfg.volumes)} ''; configFile = pkgs.writeText "copyparty.conf" configText; replaceAccountPassword = name: _account: '' ${lib.getExe pkgs.replace-secret} ${lib.escapeShellArg (passwordPlaceholder name)} "$CREDENTIALS_DIRECTORY/${credentialName name}" /run/copyparty/copyparty.conf ''; in { options.services.copyparty = { enable = lib.mkEnableOption "Copyparty"; package = lib.mkPackageOption pkgs "copyparty" { }; user = lib.mkOption { type = lib.types.str; default = "copyparty"; description = "User account that runs Copyparty."; }; group = lib.mkOption { type = lib.types.str; default = "copyparty"; description = "Group account that runs Copyparty."; }; stateDir = lib.mkOption { type = lib.types.str; default = "/var/lib/copyparty"; description = "Directory where Copyparty stores service state."; }; settings = lib.mkOption { type = lib.types.attrs; default = { }; description = "Copyparty global settings rendered in the [global] config section."; }; globalExtraConfig = lib.mkOption { type = lib.types.lines; default = ""; description = "Extra Copyparty global config lines appended after rendered global settings."; }; accounts = lib.mkOption { type = lib.types.attrsOf ( lib.types.submodule { options.passwordFile = lib.mkOption { type = lib.types.str; description = "Runtime file containing this Copyparty account's password."; }; } ); default = { }; description = "Copyparty accounts keyed by account name."; }; volumes = lib.mkOption { type = lib.types.attrsOf ( lib.types.submodule { options = { path = lib.mkOption { type = lib.types.externalPath; description = "Filesystem path served by this volume."; }; access = lib.mkOption { type = lib.types.attrsOf (lib.types.listOf lib.types.str); default = { }; description = "Copyparty access map from permission string to account names."; }; readOnly = lib.mkOption { type = lib.types.bool; default = false; description = "Bind this volume read-only into the Copyparty systemd service."; }; flags = lib.mkOption { type = lib.types.attrs; default = { }; description = "Copyparty volume flags."; }; }; } ); default = { }; description = "Copyparty volumes keyed by their URL path."; }; }; config = lib.mkIf cfg.enable { assertions = [ { assertion = cfg.accounts != { }; message = "services.copyparty.accounts must declare at least one account."; } { assertion = cfg.volumes != { }; message = "services.copyparty.volumes must declare at least one served volume."; } ]; users.groups = lib.mkIf (cfg.group == "copyparty") { copyparty = { }; }; users.users.${cfg.user} = { isSystemUser = true; inherit (cfg) group; home = cfg.stateDir; }; systemd.services.copyparty = { description = "Copyparty file server"; after = [ "network.target" ]; wantedBy = [ "multi-user.target" ]; preStart = '' ${lib.getExe' pkgs.coreutils "install"} -m 600 ${configFile} /run/copyparty/copyparty.conf ${lib.concatStringsSep "\n" (lib.mapAttrsToList replaceAccountPassword cfg.accounts)} ''; serviceConfig = { ReloadSignal = "SIGUSR1"; Type = "simple"; User = cfg.user; Group = cfg.group; WorkingDirectory = cfg.stateDir; Environment = [ "HOME=${cfg.stateDir}" "XDG_CONFIG_HOME=${cfg.stateDir}" ]; StateDirectory = "copyparty"; RuntimeDirectory = "copyparty"; RuntimeDirectoryMode = "0700"; LoadCredential = lib.mapAttrsToList ( name: account: "${credentialName name}:${account.passwordFile}" ) cfg.accounts; ExecStart = "${lib.getExe cfg.package} -c /run/copyparty/copyparty.conf"; Restart = "on-failure"; NoNewPrivileges = true; PrivateTmp = true; ProtectSystem = "strict"; ProtectHome = true; ReadWritePaths = [ cfg.stateDir ] ++ map (volume: volume.path) writableVolumes; }; }; }; }