{ config, lib, pkgs, ... }: let inherit (builtins) listToAttrs getAttr; inherit (lib.modules) mkIf mkMerge; inherit (lib.options) mkOption mkEnableOption mdDoc; inherit (lib.strings) optionalString; inherit (lib.attrsets) attrValues; inherit (lib) types; cfg = config.services.postgreSQLRemoteBackup; in { options = { # TODO: add host, port, user options services.postgreSQLRemoteBackup = { enable = mkEnableOption (mdDoc "PostgreSQL database dumps"); keepPrev = mkOption { default = true; type = types.bool; description = mdDoc '' Keep the previous run's backups but rename them to $name.prev ''; }; startAt = mkOption { default = "4h"; type = with types; either (listOf str) str; description = mdDoc '' This option defines (see `systemd.time` for format) when the databases should be dumped. The default is run every 4 hours. ''; }; backupUser = mkOption { default = "root"; type = types.str; description = mdDoc '' User which will be used for backup job and files ''; }; databases = mkOption { default = []; type = types.listOf types.str; description = mdDoc '' List of database names to dump. ''; }; location = mkOption { default = "/var/backup/postgresql"; type = types.path; description = mdDoc '' Path of directory where the PostgreSQL database dumps will be placed. ''; }; pgdumpOptions = mkOption { type = types.separatedString " "; default = "-C"; description = mdDoc '' Command line options for pg_dump. ''; }; compression = mkOption { type = types.enum ["none" "zstd"]; default = "zstd"; description = mdDoc '' The type of compression to use on the generated database dump. ''; }; compressionLevel = mkOption { type = types.int; default = 9; description = mdDoc '' The compression level used when compression is enabled. zstd accepts levels 1 to 19. ''; }; }; }; config = mkMerge [ (mkIf cfg.enable { systemd.tmpfiles.rules = [ "d '${cfg.location}' 0700 ${cfg.backupUser} - - -" ]; }) (mkIf cfg.enable { systemd.services = listToAttrs (map (db: { name = "remotePostgreSQLBackup-${db}"; value = let compressSuffixes = { "none" = ""; "zstd" = ".zstd"; }; compressSuffix = getAttr cfg.compression compressSuffixes; compressCmd = getAttr cfg.compression { "none" = "cat"; "zstd" = "${pkgs.zstd}/bin/zstd -c -${toString cfg.compressionLevel}"; }; mkSqlPath = prefix: suffix: "${cfg.location}/${db}${prefix}.sql${suffix}"; curFile = mkSqlPath "" compressSuffix; prevFile = mkSqlPath ".prev" compressSuffix; prevFiles = map (mkSqlPath ".prev") (attrValues compressSuffixes); inProgressFile = mkSqlPath ".in-progress" compressSuffix; in { enable = true; description = "Backup of ${db} database(s)"; requires = mkIf config.services.postgresql.enable [ "postgresql.service" ]; path = [ pkgs.coreutils (let pgCfg = config.services.postgresql; in if pgCfg.enable then pgCfg.package else pkgs.postgresql) ]; script = '' set -e -o pipefail umask 0077 # ensure backup is only readable by backup user ${optionalString cfg.keepPrev '' if [ -e ${curFile} ]; then rm -f ${toString prevFiles} mv ${curFile} ${prevFile} fi ''} pg_dump ${cfg.pgdumpOptions} ${db} \ | ${compressCmd} \ > ${inProgressFile} mv ${inProgressFile} ${curFile} ''; serviceConfig = { Type = "oneshot"; User = cfg.backupUser; }; inherit (cfg) startAt; }; }) cfg.databases); }) ]; }