Files
dev/configs/nixos/modules/services/copyparty/nixos-module.nix

226 lines
6.3 KiB
Nix

{
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;
};
};
};
}