{ config, lib, pkgs, ... }: let cfg = config.clan.localbackup; rsnapshotConfig = target: states: '' config_version 1.2 snapshot_root ${target.directory} sync_first 1 cmd_cp ${pkgs.coreutils}/bin/cp cmd_rm ${pkgs.coreutils}/bin/rm cmd_rsync ${pkgs.rsync}/bin/rsync cmd_ssh ${pkgs.openssh}/bin/ssh cmd_logger ${pkgs.inetutils}/bin/logger cmd_du ${pkgs.coreutils}/bin/du cmd_rsnapshot_diff ${pkgs.rsnapshot}/bin/rsnapshot-diff ${lib.optionalString (target.preBackupHook != null) '' cmd_preexec ${pkgs.writeShellScript "preexec.sh" '' set -efu -o pipefail ${target.preBackupHook} ''} ''} ${lib.optionalString (target.postBackupHook != null) '' cmd_postexec ${pkgs.writeShellScript "postexec.sh" '' set -efu -o pipefail ${target.postBackupHook} ''} ''} retain snapshot ${builtins.toString config.clan.localbackup.snapshots} ${lib.concatMapStringsSep "\n" (state: '' ${lib.concatMapStringsSep "\n" (folder: '' backup ${folder} ${config.networking.hostName}/ '') state.folders} '') states} ''; in { options.clan.localbackup = { targets = lib.mkOption { type = lib.types.attrsOf ( lib.types.submodule ( { name, ... }: { options = { name = lib.mkOption { type = lib.types.strMatching "^[a-zA-Z0-9._-]+$"; default = name; description = "the name of the backup job"; }; directory = lib.mkOption { type = lib.types.str; description = "the directory to backup"; }; mountpoint = lib.mkOption { type = lib.types.nullOr lib.types.str; default = null; description = "mountpoint of the directory to backup. If set, the directory will be mounted before the backup and unmounted afterwards"; }; preMountHook = lib.mkOption { type = lib.types.nullOr lib.types.lines; default = null; description = "Shell commands to run before the directory is mounted"; }; postMountHook = lib.mkOption { type = lib.types.nullOr lib.types.lines; default = null; description = "Shell commands to run after the directory is mounted"; }; preUnmountHook = lib.mkOption { type = lib.types.nullOr lib.types.lines; default = null; description = "Shell commands to run before the directory is unmounted"; }; postUnmountHook = lib.mkOption { type = lib.types.nullOr lib.types.lines; default = null; description = "Shell commands to run after the directory is unmounted"; }; preBackupHook = lib.mkOption { type = lib.types.nullOr lib.types.lines; default = null; description = "Shell commands to run before the backup"; }; postBackupHook = lib.mkOption { type = lib.types.nullOr lib.types.lines; default = null; description = "Shell commands to run after the backup"; }; }; } ) ); default = { }; description = "List of directories where backups are stored"; }; snapshots = lib.mkOption { type = lib.types.int; default = 20; description = "Number of snapshots to keep"; }; }; config = let mountHook = target: '' if [[ -x /run/current-system/sw/bin/localbackup-mount-${target.name} ]]; then /run/current-system/sw/bin/localbackup-mount-${target.name} fi if [[ -x /run/current-system/sw/bin/localbackup-unmount-${target.name} ]]; then trap "/run/current-system/sw/bin/localbackup-unmount-${target.name}" EXIT fi ''; in lib.mkIf (cfg.targets != { }) { environment.systemPackages = [ (pkgs.writeShellScriptBin "localbackup-create" '' set -efu -o pipefail export PATH=${ lib.makeBinPath [ pkgs.rsnapshot pkgs.coreutils pkgs.util-linux ] } ${lib.concatMapStringsSep "\n" (target: '' ( ${mountHook target} echo "Creating backup '${target.name}'" rsnapshot -c "${pkgs.writeText "rsnapshot.conf" (rsnapshotConfig target (lib.attrValues config.clanCore.state))}" sync rsnapshot -c "${pkgs.writeText "rsnapshot.conf" (rsnapshotConfig target (lib.attrValues config.clanCore.state))}" snapshot ) '') (builtins.attrValues cfg.targets)} '') (pkgs.writeShellScriptBin "localbackup-list" '' set -efu -o pipefail export PATH=${ lib.makeBinPath [ pkgs.jq pkgs.findutils pkgs.coreutils pkgs.util-linux ] } (${ lib.concatMapStringsSep "\n" (target: '' ( ${mountHook target} find ${lib.escapeShellArg target.directory} -mindepth 1 -maxdepth 1 -name "snapshot.*" -print0 -type d \ | jq -Rs 'split("\u0000") | .[] | select(. != "") | { "name": ("${target.name}::" + .)}' ) '') (builtins.attrValues cfg.targets) }) | jq -s . '') (pkgs.writeShellScriptBin "localbackup-restore" '' set -efu -o pipefail export PATH=${ lib.makeBinPath [ pkgs.rsync pkgs.coreutils pkgs.util-linux pkgs.gawk ] } name=$(awk -F'::' '{print $1}' <<< $NAME) backupname=''${NAME#$name::} if command -v localbackup-mount-$name; then localbackup-mount-$name fi if command -v localbackup-unmount-$name; then trap "localbackup-unmount-$name" EXIT fi if [[ ! -d $backupname ]]; then echo "No backup found $backupname" exit 1 fi IFS=';' read -ra FOLDER <<< "$FOLDERS" for folder in "''${FOLDER[@]}"; do rsync -a "$backupname/${config.networking.hostName}$folder/" "$folder" done '') ] ++ (lib.mapAttrsToList ( name: target: pkgs.writeShellScriptBin ("localbackup-mount-" + name) '' set -efu -o pipefail ${lib.optionalString (target.preMountHook != null) target.preMountHook} ${lib.optionalString (target.mountpoint != null) '' if ! ${pkgs.util-linux}/bin/mountpoint -q ${lib.escapeShellArg target.mountpoint}; then ${pkgs.util-linux}/bin/mount -o X-mount.mkdir ${lib.escapeShellArg target.mountpoint} fi ''} ${lib.optionalString (target.postMountHook != null) target.postMountHook} '' ) cfg.targets) ++ lib.mapAttrsToList ( name: target: pkgs.writeShellScriptBin ("localbackup-unmount-" + name) '' set -efu -o pipefail ${lib.optionalString (target.preUnmountHook != null) target.preUnmountHook} ${lib.optionalString ( target.mountpoint != null ) "${pkgs.util-linux}/bin/umount ${lib.escapeShellArg target.mountpoint}"} ${lib.optionalString (target.postUnmountHook != null) target.postUnmountHook} '' ) cfg.targets; clanCore.backups.providers.localbackup = { # TODO list needs to run locally or on the remote machine list = "localbackup-list"; create = "localbackup-create"; restore = "localbackup-restore"; }; }; }