{ description = "Restic"; inputs = { nixpkgs = { url = "nixpkgs/nixos-unstable"; }; flake-utils = { url = "github:numtide/flake-utils"; }; }; outputs = { self, nixpkgs, flake-utils, ... }@inputs: flake-utils.lib.eachDefaultSystem (system: let pkgs = import nixpkgs { inherit system; }; in { packages = { restic = let pname = "restic"; version = "0.15.1"; sha256 = "sha256-KdPslVJHH+xdUuFfmLZumP2lHzkDrrAvpDaj38SuP8o="; vendorSha256 = "sha256-oetaCiXWEBUEf382l4sjO0SCPxkoh+bMTgIf/qJTQms="; in pkgs.buildGoModule { inherit pname version; src = pkgs.fetchFromGitHub { owner = "restic"; repo = "restic"; rev = "v${version}"; sha256 = sha256; }; patches = [ # The TestRestoreWithPermissionFailure test fails in Nix’s build sandbox ./0001-Skip-testing-restore-with-permission-failure.patch ]; vendorSha256 = vendorSha256; subPackages = [ "cmd/restic" ]; nativeBuildInputs = [ pkgs.installShellFiles pkgs.makeWrapper ]; passthru.tests.restic = pkgs.nixosTests.restic; postPatch = '' rm cmd/restic/integration_fuse_test.go ''; postInstall = '' wrapProgram $out/bin/restic --prefix PATH : '${pkgs.rclone}/bin' '' + pkgs.lib.optionalString (pkgs.stdenv.hostPlatform == pkgs.stdenv.buildPlatform) '' $out/bin/restic generate \ --bash-completion restic.bash \ --zsh-completion restic.zsh \ --man . installShellCompletion restic.{bash,zsh} installManPage *.1 ''; meta = with pkgs.lib; { homepage = "https://restic.net"; description = "A backup program that is fast, efficient and secure"; platforms = platforms.linux ++ platforms.darwin; license = licenses.bsd2; maintainers = [ maintainers.mbrgm ]; }; }; }; } ) // { nixosModules = { restic = { config, lib, pkgs, ... }: let cfg = config.restic; package = self.packages.${pkgs.system}.restic; in { options = { restic = lib.options.mkOption { type = lib.types.submodule { options = { enable = lib.options.mkEnableOption "Restic"; password = lib.options.mkOption { type = lib.types.str; }; storage = lib.options.mkOption { type = lib.types.enum [ "azure" "b2" ]; # default = "b2"; }; healthcheck = lib.options.mkOption { type = lib.types.submodule { options = { enable = lib.options.mkEnableOption "use healthcheck"; api_url = lib.options.mkOption { type = lib.types.str; }; api_key = lib.options.mkOption { type = lib.types.str; }; timeout = lib.options.mkOption { type = lib.types.int; default = 86400; }; grace = lib.options.mkOption { type = lib.types.int; default = 14400; }; }; }; }; b2 = lib.options.mkOption { type = lib.types.submodule { options = { bucket = lib.options.mkOption { type = lib.types.str; }; account_id = lib.options.mkOption { type = lib.types.str; }; account_key = lib.options.mkOption { type = lib.types.str; }; }; }; }; azure = lib.options.mkOption { type = lib.types.submodule { options = { account_name = lib.options.mkOption { type = lib.types.str; }; account_key = lib.options.mkOption { type = lib.types.str; }; }; }; }; backups = lib.options.mkOption { type = lib.types.listOf ( lib.types.submodule { options = { one-file-system = lib.options.mkOption { type = lib.types.bool; default = false; }; excludes = lib.options.mkOption { type = lib.types.listOf lib.types.str; default = [ ]; }; paths = lib.options.mkOption { type = lib.types.listOf lib.types.str; }; }; } ); }; }; }; }; }; config = let bucket = { "b2" = cfg.b2.bucket; "azure" = builtins.replaceStrings [ "." ] [ "-" ] "${config.networking.hostName}.${config.networking.domain}"; }.${cfg.storage}; directory = { "b2" = "${config.networking.hostName}.${config.networking.domain}"; "azure" = "/"; }.${cfg.storage}; in lib.mkIf cfg.enable { environment.systemPackages = [ package ]; environment.etc."restic/password" = { text = cfg.password; user = "root"; group = "root"; mode = "0400"; }; environment.etc."restic/environment" = let hcConfig = if cfg.healthcheck.enable then '' HC_API_KEY=${cfg.healthcheck.api_key} HC_API_URL=${cfg.healthcheck.api_url} '' else ""; storageConfig = { "b2" = '' B2_ACCOUNT_ID=${cfg.b2.account_id} B2_ACCOUNT_KEY=${cfg.b2.account_key} ''; "azure" = '' AZURE_ACCOUNT_NAME=${cfg.azure.account_name} AZURE_ACCOUNT_KEY=${cfg.azure.account_key} ''; }.${cfg.storage}; repositoryConfig = { "b2" = "b2:${bucket}:${directory}"; "azure" = "azure:${bucket}:${directory}"; }.${cfg.storage}; in { text = '' ${hcConfig} RESTIC_CACHE_DIR=/var/cache/restic RESTIC_PASSWORD_FILE=/etc/restic/password RESTIC_REPOSITORY=${repositoryConfig} ${storageConfig} ''; user = "root"; group = "root"; mode = "0400"; }; environment.etc."restic/rclone" = { text = { "b2" = '' [b2] type = b2 account = ${cfg.b2.account_id} key = ${cfg.b2.account_key} ''; "azure" = '' [azure] type = azureblob account = ${cfg.azure.account_name} key = ${cfg.azure.account_key} ''; }.${cfg.storage}; user = "root"; group = "root"; mode = "0400"; }; systemd.services.restic = let curlOptions = "--fail --silent --show-error --max-time 10 --retry 5"; hcPayload = if cfg.healthcheck.enable then builtins.toJSON { name = "${config.networking.hostName}.${config.networking.domain}"; timeout = cfg.healthcheck.timeout; grace = cfg.healthcheck.grace; unique = [ "name" ]; channels = "*"; } else ""; hcSetup = if cfg.healthcheck.enable then '' HC_PAYLOAD='${hcPayload}' HC_URL=$(${pkgs.curl}/bin/curl ${curlOptions} --request POST --header "X-Api-Key: $HC_API_KEY" --data "$HC_PAYLOAD" $HC_API_URL | ${pkgs.jq}/bin/jq -r .ping_url) '' else ""; hcStart = if cfg.healthcheck.enable then '' if [ ! -z "$HC_URL" ] then ${pkgs.curl}/bin/curl ${curlOptions} --output /dev/null $HC_URL/start || true fi '' else ""; hcStop = if cfg.healthcheck.enable then '' if [ ! -z "$HC_URL" ] then ${pkgs.curl}/bin/curl ${curlOptions} --output /dev/null $HC_URL || true fi '' else ""; backupCommands = lib.strings.concatStringsSep "\n" ( map ( backup: let one-file-system = if backup.one-file-system then " --one-file-system" else ""; excludes = lib.strings.concatMapStrings ( arg: " --exclude=${arg}" ) backup.excludes; paths = lib.strings.concatStringsSep " " backup.paths; in "${package}/bin/restic backup${one-file-system}${excludes} ${paths}" ) cfg.backups ); initCheck = { "b2" = '' config=$(${pkgs.rclone}/bin/rclone --config /etc/restic/rclone lsjson "b2:${bucket}/${directory}/config" | ${pkgs.jq}/bin/jq -r '. | length | . > 0') if [ "$config" != "true" ] then ${package}/bin/restic init fi ''; "azure" = '' container=$(${pkgs.rclone}/bin/rclone --config /etc/restic/rclone lsjson "azure:" | ${pkgs.jq}/bin/jq -r 'map(select(.Path=="${bucket}" and .IsBucket)) | length | . > 0') if [ "$container" != "true" ] then ${pkgs.rclone}/bin/rclone --config /etc/restic/rclone mkdir "azure:${bucket}" fi config=$(${pkgs.rclone}/bin/rclone --config /etc/restic/rclone lsjson "azure:${bucket}/config" | ${pkgs.jq}/bin/jq -r '. | length | . > 0') if [ "$config" != "true" ] then ${package}/bin/restic init fi ''; }.${cfg.storage}; in { serviceConfig = { Type = "oneshot"; CacheDirectory = "restic"; EnvironmentFile = "/etc/restic/environment"; }; script = '' ${hcSetup} ${hcStart} ${initCheck} ${backupCommands} ${package}/bin/restic forget --prune --keep-daily 14 ${package}/bin/restic check ${package}/bin/restic cache --cleanup ${hcStop} ''; }; systemd.timers.restic = { timerConfig = { OnCalendar = "*-*-* 02:00:00"; RandomizedDelaySec = "60m"; }; wantedBy = [ "multi-user.target" ]; }; }; }; }; }; }