{ config, lib, pkgs, util, ... }: let cfg = config.modules.services.copyparty; accountName = config.user.name; serviceGroup = "storage"; serviceStateDir = "/var/lib/copyparty"; serviceUser = "copyparty"; writableVolumes = lib.filter (volume: !volume.readOnly) (lib.attrValues cfg.volumes); port = 3923; defaultAccess = { A = [ accountName ]; }; defaultWritableFlags = { d2d = true; d2t = true; dthumb = true; }; credentialHash = builtins.substring 0 16 (builtins.hashString "sha256" accountName); credentialName = "password-${credentialHash}"; passwordPlaceholder = "@COPYPARTY_PASSWORD_${credentialHash}@"; globalSettings = { i = "0.0.0.0"; usernames = true; "http-only" = true; "no-crt" = true; "no-thumb" = true; "no-snap" = true; "no-rescan" = true; "ses-db" = "${serviceStateDir}/sessions.db"; "chpw-db" = "${serviceStateDir}/chpw.json"; "shr-db" = "${serviceStateDir}/shares.db"; } // lib.optionalAttrs cfg.reverseProxy.enable { rproxy = 1; "xff-hdr" = "x-forwarded-for"; }; globalExtraConfig = lib.optionalString cfg.reverseProxy.enable ( lib.concatMapStringsSep "\n" (source: "xff-src: ${source}") cfg.reverseProxy.trustedSources ); volumeFlags = flags: lib.optionalAttrs (flags != { }) ( { chmod_d = "2770"; chmod_f = "660"; } // flags ); renderedVolumes = lib.mapAttrs (_urlPath: volume: { inherit (volume) path; access = if volume.readOnly then { r = [ accountName ]; } else defaultAccess // volume.access; flags = volumeFlags (lib.optionalAttrs (!volume.readOnly) defaultWritableFlags // volume.flags); }) cfg.volumes; configText = util.formats.copyparty.render { inherit globalExtraConfig; accounts.${accountName} = passwordPlaceholder; global = globalSettings; volumes = renderedVolumes; }; configFile = pkgs.writeText "copyparty.conf" configText; replacePassword = '' ${lib.getExe pkgs.replace-secret} ${lib.escapeShellArg passwordPlaceholder} "$CREDENTIALS_DIRECTORY/${credentialName}" /run/copyparty/copyparty.conf ''; in { options.modules.services.copyparty = { enable = lib.mkEnableOption "Copyparty"; passwordFile = lib.mkOption { type = lib.types.str; description = "Sets the runtime file that contains the Copyparty account password."; }; volumes = lib.mkOption { type = lib.types.attrsOf ( lib.types.submodule { options = { path = lib.mkOption { type = lib.types.externalPath; description = "Sets the filesystem path served by this volume."; }; access = lib.mkOption { type = lib.types.attrsOf (lib.types.listOf lib.types.str); default = { }; example = { A = [ "alexion" ]; }; description = "Maps Copyparty permission strings to account names."; }; readOnly = lib.mkOption { type = lib.types.bool; default = false; description = "Serves this volume read-only."; }; flags = lib.mkOption { type = lib.types.attrs; default = { }; example = { dthumb = true; }; description = "Sets Copyparty volume flags."; }; }; } ); default = { }; description = "Defines Copyparty volumes by URL path."; }; reverseProxy = { enable = lib.mkEnableOption "reverse-proxy trust settings for Copyparty"; trustedSources = lib.mkOption { type = lib.types.listOf lib.types.str; default = [ ]; description = "Lists proxy sources that can set Copyparty's forwarded client IP header."; }; }; }; config = lib.mkMerge [ { registry."stable-uid".${config.networking.hostName}.copyparty = 20002; } (lib.mkIf cfg.enable { assertions = [ { assertion = cfg.volumes != { }; message = "modules.services.copyparty.volumes must declare at least one served volume."; } { assertion = !cfg.reverseProxy.enable || cfg.reverseProxy.trustedSources != [ ]; message = "modules.services.copyparty.reverseProxy.trustedSources must name at least one trusted proxy source when reverse-proxy support is enabled."; } ]; users.users.${serviceUser} = { isSystemUser = true; uid = config.registry."stable-uid".${config.networking.hostName}.copyparty; group = serviceGroup; home = serviceStateDir; }; 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 ${replacePassword} ''; serviceConfig = { ReloadSignal = "SIGUSR1"; Type = "simple"; User = serviceUser; Group = serviceGroup; WorkingDirectory = serviceStateDir; Environment = [ "HOME=${serviceStateDir}" "XDG_CONFIG_HOME=${serviceStateDir}" ]; StateDirectory = "copyparty"; RuntimeDirectory = "copyparty"; RuntimeDirectoryMode = "0700"; LoadCredential = [ "${credentialName}:${cfg.passwordFile}" ]; ExecStart = "${lib.getExe pkgs.copyparty-min} -c /run/copyparty/copyparty.conf"; Restart = "on-failure"; NoNewPrivileges = true; PrivateTmp = true; ProtectSystem = "strict"; ProtectHome = true; ReadWritePaths = [ serviceStateDir ] ++ map (volume: volume.path) writableVolumes; }; }; networking.firewall.allowedTCPPorts = [ port ]; }) ]; }