diff --git a/configs/nixos/modules/services/copyparty/default.nix b/configs/nixos/modules/services/copyparty/default.nix index a5e34ab..27b2540 100644 --- a/configs/nixos/modules/services/copyparty/default.nix +++ b/configs/nixos/modules/services/copyparty/default.nix @@ -2,18 +2,81 @@ config, lib, pkgs, + util, ... }: let cfg = config.modules.services.copyparty; # NOLINT(let_order) - port = 3923; - serviceGroup = "storage"; serviceStateDir = "/var/lib/copyparty"; serviceUser = "copyparty"; + + writableVolumes = lib.filter (volume: !volume.readOnly) (lib.attrValues cfg.volumes); + + port = 3923; + + accountName = config.user.name; + + credentialName = "password-${builtins.substring 0 16 (builtins.hashString "sha256" accountName)}"; + + passwordPlaceholder = "@COPYPARTY_PASSWORD_${ + builtins.substring 0 16 (builtins.hashString "sha256" accountName) + }@"; + + 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) + access + path + ; + flags = volumeFlags 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 = { @@ -91,44 +154,47 @@ in } ]; - services.copyparty = { - enable = true; + users.users.${serviceUser} = { + isSystemUser = true; + uid = config.registry."stable-uid".${config.networking.hostName}.copyparty; group = serviceGroup; - package = pkgs.copyparty-min; - stateDir = serviceStateDir; - user = serviceUser; - settings = { - 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"; - }; - accounts.${config.user.name}.passwordFile = cfg.passwordFile; - globalExtraConfig = lib.optionalString cfg.reverseProxy.enable ( - lib.concatMapStringsSep "\n" (source: "xff-src: ${source}") cfg.reverseProxy.trustedSources - ); - volumes = lib.mapAttrs (_urlPath: volume: { - inherit (volume) - access - flags - path - readOnly - ; - }) cfg.volumes; + home = serviceStateDir; }; - users.users.${serviceUser}.uid = - config.registry."stable-uid".${config.networking.hostName}.copyparty; + 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 ]; }) diff --git a/configs/nixos/modules/services/copyparty/nixos-module.nix b/configs/nixos/modules/services/copyparty/nixos-module.nix deleted file mode 100644 index affc68d..0000000 --- a/configs/nixos/modules/services/copyparty/nixos-module.nix +++ /dev/null @@ -1,225 +0,0 @@ -{ - 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 = "Sets the user that runs Copyparty."; - }; - - group = lib.mkOption { - type = lib.types.str; - default = "copyparty"; - description = "Sets the group that runs Copyparty."; - }; - - stateDir = lib.mkOption { - type = lib.types.str; - default = "/var/lib/copyparty"; - description = "Sets the directory where Copyparty stores service state."; - }; - - settings = lib.mkOption { - type = lib.types.attrs; - default = { }; - description = "Defines Copyparty settings for the `[global]` config section."; - }; - - globalExtraConfig = lib.mkOption { - type = lib.types.lines; - default = ""; - description = "Appends extra lines after rendered global settings."; - }; - - accounts = lib.mkOption { - type = lib.types.attrsOf ( - lib.types.submodule { - options.passwordFile = lib.mkOption { - type = lib.types.str; - description = "Sets the runtime file that contains this account's password."; - }; - } - ); - default = { }; - description = "Defines Copyparty accounts by account name."; - }; - - 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 = { }; - description = "Maps Copyparty permission strings to account names."; - }; - - readOnly = lib.mkOption { - type = lib.types.bool; - default = false; - description = "Binds this volume read-only into the Copyparty systemd service."; - }; - - flags = lib.mkOption { - type = lib.types.attrs; - default = { }; - description = "Sets Copyparty volume flags."; - }; - }; - } - ); - default = { }; - description = "Defines Copyparty volumes by 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; - }; - }; - }; -} diff --git a/src/dotfiles/default.nix b/src/dotfiles/default.nix index 1f7d642..c7421ac 100644 --- a/src/dotfiles/default.nix +++ b/src/dotfiles/default.nix @@ -14,7 +14,7 @@ let registry = registry.collected; }; - util = import ./util.nix { inherit lib; }; + util = import ./util { inherit lib; }; registry = import ./registry.nix { inherit lib util; diff --git a/src/dotfiles/util.nix b/src/dotfiles/util/default.nix similarity index 94% rename from src/dotfiles/util.nix rename to src/dotfiles/util/default.nix index 01c44ff..0615a04 100644 --- a/src/dotfiles/util.nix +++ b/src/dotfiles/util/default.nix @@ -21,8 +21,9 @@ let value: entries: builtins.length (lib.unique (map value entries)) == builtins.length entries; in { - inherit collectNixFiles; + formats.copyparty = import ./format-copyparty.nix { inherit lib; }; + inherit collectNixFiles; removeNixSuffix = path: lib.removeSuffix ".nix" (builtins.baseNameOf path); glyph = diff --git a/src/dotfiles/util/format-copyparty.nix b/src/dotfiles/util/format-copyparty.nix new file mode 100644 index 0000000..4c5823d --- /dev/null +++ b/src/dotfiles/util/format-copyparty.nix @@ -0,0 +1,58 @@ +{ lib }: +let + 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" volume.flags} + ''; +in +{ + render = + { + accounts, + global, + volumes, + globalExtraConfig ? "", + }: + '' + ${renderSection "global" global} + ${globalExtraConfig} + ${renderSection "accounts" accounts} + ${lib.concatStringsSep "\n" (lib.mapAttrsToList renderVolume volumes)} + ''; +}