├── .devcontainer └── devcontainer.json ├── .editorconfig ├── .github └── workflows │ ├── linter.yml │ └── stale.yml ├── .gitignore ├── .semver.yaml ├── CHANGELOG.md ├── LICENSE ├── Library └── LaunchAgents │ ├── com.github.erikw.restic-backup.plist │ └── com.github.erikw.restic-check.plist ├── Makefile ├── OSSMETADATA ├── README.md ├── ScheduledTask ├── install.ps1 └── uninstall.ps1 ├── bin ├── cron_mail ├── nm-unmetered-connection.sh ├── restic_backup.sh ├── restic_check.sh ├── resticw └── systemd-email ├── etc ├── cron.d │ └── restic └── restic │ ├── _global.env.sh │ ├── backup_exclude.txt │ ├── default.env.sh │ └── pw.txt ├── img ├── macos_notification.png ├── pen-paper.png ├── plus.png ├── readme_sections.png └── tasksched.png ├── scripts └── devcontainer_postCreateCommand.sh └── usr └── lib └── systemd └── system ├── nm-unmetered-connection.service ├── restic-backup@.service ├── restic-backup@.timer ├── restic-check@.service ├── restic-check@.timer └── status-email-user@.service /.devcontainer/devcontainer.json: -------------------------------------------------------------------------------- 1 | { 2 | "features": { 3 | "ghcr.io/devcontainers-extra/features/apt-get-packages:1": { 4 | "packages": ["shellcheck"] 5 | } 6 | }, 7 | "postCreateCommand": "bash scripts/devcontainer_postCreateCommand.sh", 8 | "customizations": { 9 | "vscode": { 10 | "extensions": [ 11 | "rogalmic.bash-debug", 12 | "timonwong.shellcheck", 13 | "mads-hartmann.bash-ide-vscode" 14 | ] 15 | } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = tab 5 | indent_size = 4 6 | end_of_line = lf 7 | charset = utf-8 8 | trim_trailing_whitespace = true 9 | insert_final_newline = true 10 | #max_line_length = 120 11 | 12 | [Makefile] 13 | # Enforce tab (Makefiles require tabs) 14 | indent_style = tab 15 | 16 | [*.md] 17 | trim_trailing_whitespace = false 18 | -------------------------------------------------------------------------------- /.github/workflows/linter.yml: -------------------------------------------------------------------------------- 1 | name: Lint 2 | 3 | on: 4 | workflow_dispatch: 5 | push: 6 | branches: main 7 | paths: 8 | - "**.sh" 9 | - ".github/workflows/linter.yml" 10 | - "bin/**" 11 | pull_request: 12 | branches: main 13 | paths: 14 | - "**.sh" 15 | - ".github/workflows/linter.yml" 16 | - "bin/**" 17 | 18 | permissions: {} 19 | 20 | jobs: 21 | build: 22 | name: Lint 23 | runs-on: ubuntu-latest 24 | 25 | permissions: 26 | contents: read 27 | packages: read 28 | # To report GitHub Actions status checks 29 | statuses: write 30 | 31 | steps: 32 | - name: Checkout Code 33 | uses: actions/checkout@v4 34 | with: 35 | # super-linter needs the full git history to get the 36 | # list of files that changed across commits 37 | fetch-depth: 0 38 | 39 | - name: Super-linter 40 | uses: super-linter/super-linter@v7.3.0 41 | env: 42 | # To report GitHub Actions status checks 43 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 44 | DEFAULT_BRANCH: main 45 | VALIDATE_ALL_CODEBASE: true 46 | VALIDATE_BASH: true 47 | IGNORE_GENERATED_FILES: true 48 | -------------------------------------------------------------------------------- /.github/workflows/stale.yml: -------------------------------------------------------------------------------- 1 | name: "Close stale issues and PRs" 2 | on: 3 | workflow_dispatch: 4 | schedule: 5 | - cron: "0 0 * * 0" 6 | 7 | jobs: 8 | stale: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - uses: actions/stale@v8 12 | with: 13 | days-before-stale: 180 14 | exempt-issue-labels: "NotStale" 15 | exempt-pr-labels: "NotStale" 16 | stale-issue-message: "Issue is stale; will soon close." 17 | stale-pr-message: "PR is stale; will soon close." 18 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # make install 2 | /build 3 | 4 | # IntelliJ 5 | .idea/ 6 | *.iml 7 | # VSCode 8 | .vscode/ 9 | -------------------------------------------------------------------------------- /.semver.yaml: -------------------------------------------------------------------------------- 1 | alpha: 0 2 | beta: 0 3 | rc: 0 4 | release: v7.4.0 5 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | All notable changes to this project will be documented in this file. 3 | 4 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), 5 | and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). 6 | 7 | ## [Unreleased] 8 | ### Added 9 | - Debug scripts by setting `TRACE=1`. 10 | - Add semver-cli for git tagging. 11 | ### Changed 12 | - Warn on certain unset envvars instead of error-exit. 13 | 14 | ## [7.4.0] - 2023-03-08 15 | ### Added 16 | - Support saving hourly snapshots. [#98](https://github.com/erikw/restic-automatic-backup-scheduler/pull/98) 17 | - Support for pre backup script at /etc/restic/pre_backup.sh [107](https://github.com/erikw/restic-automatic-backup-scheduler/pull/107) 18 | 19 | ### Fixed 20 | - Full path to `/bin/bash` in sytemd services. [#96](https://github.com/erikw/restic-automatic-backup-scheduler/issues/96) 21 | 22 | ## [7.3.4] - 2022-04-29 23 | ### Fixed 24 | - Backup stats notifications: fix issue where `restic snapshots --latest 2` will show more than two snapshots due to different backup paths used. 25 | 26 | ## [7.3.3] - 2022-04-14 27 | ### Fixed 28 | - Trying to fix broken Homebrew bottles due to GitHub API issues. 29 | 30 | ## [7.3.2] - 2022-04-11 31 | ### Fixed 32 | - Trying to fix broken Homebrew bottles 33 | 34 | ## [7.3.1] - 2022-04-11 35 | ### Fixed 36 | - `resticw` is now a true wrapper in that it support `--` args to restic. 37 | - OnFailure no longer masked by the stderr redirect to systemd-cat. [#86](https://github.com/erikw/restic-automatic-backup-scheduler/pull/86) 38 | 39 | ## [7.3.0] - 2022-02-15 40 | ### Added 41 | - optional user-controlled notification. See `RESTIC_NOTIFY_BACKUP_STATS` and in `backup.sh`. 42 | 43 | ## [7.2.0] - 2022-02-15 44 | ### Added 45 | - restic-check LaunchAgent. 46 | 47 | ### Changed 48 | - [README.md](README.md) is restructured with easier TL;DR for each OS and a more general detailed section for the interested. 49 | 50 | ## [7.1.0] - 2022-02-13 51 | ### Changed 52 | - Minimize base install. The following features are now opt-in: nm-unmetered detection, cron_mail, systemd-email. 53 | 54 | ## [7.0.0] - 2022-02-13 55 | ### Changed 56 | - Renamed project from `restic-systemd-automatic-backup` to `restic-automatic-backup-scheduler` to fit all now supported setups. 57 | 58 | ## [6.0.0] - 2022-02-12 59 | ### Added 60 | - Windows support with native ScheduledTask! New target `$ make install-schedtask` for Windows users. 61 | 62 | ## [5.3.1] - 2022-02-12 63 | ### Fixed 64 | - Launchagentdir make macro 65 | 66 | ## [5.3.0] - 2022-02-12 67 | ### Added 68 | - Allow custom launchagent dir, used by Homebrew. 69 | 70 | ## [5.2.1] - 2022-02-11 71 | ### Added 72 | - Homebrew Formula at [erikw/homebrew-tap](https://github.com/erikw/homebrew-tap). You can now install with `$ brew install erikw/tap/restic-automatic-backup-scheduler`! 73 | 74 | ### Fixed 75 | - Use default profile in LaunchAgent. 76 | 77 | ## [5.2.0] - 2022-02-11 78 | ### Added 79 | - Make option to override destination dir for configuration files. Needed for Homebrew. 80 | 81 | ### Changed 82 | - Write permissions on installed scripts removed (0755 -> 0555). Homebrew was complaining. 83 | 84 | ## [5.1.0] - 2022-02-11 85 | ### Added 86 | - macos LaunchAgent support. Install with `make install-launchagent` and activate with `make activate-launchagent`. See [README.md](README.md) for details. 87 | - make option INSTALL_PREFIX to make PKGBUILD and such easier to write. 88 | 89 | ## [5.0.0] - 2022-02-08 90 | ### Added 91 | - `resticw` wrapper for working with different profiles without the need to source the profiles first. 92 | - `$ make install-systemd` will now make a timestamped backup of any existing `/etc/restic/*` files before installing a newer version. 93 | - `$ make install-cron` for installing the cron-job. 94 | 95 | ### Changed 96 | - **BREAKING CHANGE** moved systemd installation with makefile from `/etc/systemd/system` to `/usr/lib/systemd/system` as this is what packages should do. This is to be able to simplify the arch [PKGBUILD](https://aur.archlinux.org/cgit/aur.git/tree/PKGBUILD?h=restic-automatic-backup-scheduler) so that it does not need to do anything else than `make install`. 97 | - If you upgrade form an existing install, you should disable and then re-enable the timer, so that the symlink is pointing to the new location of the timer. 98 | ```console 99 | # systemctl disable restic-backup@.timer 100 | # systemctl enable restic-backup@.timer 101 | ``` 102 | - **BREAKING CHANGE** moved script installation with makefile from `/usr/local/sbin` to `/bin` to have a simpler interface to work with `$PREFIX`. 103 | - **BREAKING CHANGE** renamed `etc/restic/*.env` files to `etc/restic/*.env.sh` to clearly communicate that it's a shell script that will be executed (source), and also hint at code editors what file this is to set corect syntax highligting etc. This also enables the shellcheck linter to work more easily on these files as well. 104 | - Renamed top level make install targets. The old `$ make install` is now `$ make install-systemd` 105 | 106 | ### Fixed 107 | - Installation with custom `PREFIX` now works properly with Make: `$ PREFIX=/usr/local make install` whill now install everything at the expected location. With this, it's easy to use this script as non-root user on e.g. an macOS system. 108 | 109 | ## [4.0.0] - 2022-02-01 110 | ### Fixed 111 | - Use arrays to build up command lines. When fixing `shellcheck(1)` errors, quotes would disable expansion on e.g. $RESTIC_BACKUP_PATHS 112 | - **BREAKING CHANGE** `RESTIC_BACKUP_PATHS` is now a string with `:` separated values 113 | 114 | ## [3.0.1] - 2022-02-01 115 | ### Fixed 116 | - Environment variable assertion should allow empty values e.g. `RESTIC_BACKUP_EXTRA_ARGS` 117 | 118 | ## [3.0.0] - 2022-02-01 119 | ### Added 120 | - Allow extra arguments to restic-backup with `$RESTIC_BACKUP_EXTRA_ARGS`. 121 | - Add `$RESTIC_VERBOSITY_LEVEL` for debugging. 122 | - Assertion on all needed environment variables in the backup and check scripts. 123 | - Added linter (`shellcheck(1)`) that is run on push and PRs. 124 | 125 | ### Changed 126 | - **BREAKING CHANGE** renamed 127 | - `/etc/restic/backup_exclude` to `/etc/restic/backup_exclude.txt` 128 | - `/.backup_exclude` to `/.backup_exclude.txt`. 129 | - **BREAKING CHANGE** renamed envvars for consistency 130 | - `BACKUP_PATHS` -> `RESTIC_BACKUP_PATHS` 131 | - `BACKUP_TAG` -> `RESTIC_BACKUP_TAG` 132 | - `RETENTION_DAYS` -> `RESTIC_RETENTION_DAYS` 133 | - `RETENTION_WEEKS` -> `RESTIC_RETENTION_WEEKS` 134 | - `RETENTION_MONTHS` -> `RESTIC_RETENTION_MONTHS` 135 | - `RETENTION_YEARS` -> `RESTIC_RETENTION_YEARS` 136 | - Align terminology used in README with the one used by B2 for credentials (keyId + applicationKey pair). 137 | 138 | ## [2.0.0] - 2022-02-01 139 | ### Changed 140 | - **BREAKING CHANGE** [#45](https://github.com/erikw/restic-automatic-backup-scheduler/pull/45): multiple backup profiles are now supported. Please backup your configuration before upgrading. The setup of configuration files are now laied out differently. See the [README.md](README.md) TL;DR setup section. 141 | - `restic_backup.sh` has had variables extracted to profiles instead, to allow for configuration of different backups on the same system. 142 | - `b2_env.sh` is split to two files `_global.env` and `default.env` (the default profile). `_global.env` will have B2 accountID and accountKey and `default.env` has backup paths, and retention. 143 | - `b2_pw.sh` renamed to pw.txt 144 | 145 | ### Fixed 146 | - `restic_backup.sh` now finds `.backup_exclude` files on each backup path as intended. 147 | - Install executeables to `$PREFIX/sbin` instead of `$PREFIX/user/local/sbin`, so that `$ PREFIX=/usr/local make install` does what is expected. 148 | 149 | ## [1.0.1] - 2021-12-03 150 | ### Fixed 151 | - $(make install) now works for the *.template files ([#40](https://github.com/erikw/restic-automatic-backup-scheduler/issues/40)) 152 | 153 | ## [1.0.0] - 2021-12-02 154 | It's time to call this a proper major version! 155 | 156 | ### Added 157 | - `uninstall` target for `Makefile` 158 | - Add `--prune` to `restic-forget` 159 | - README badges and updates. 160 | 161 | ### Fixed 162 | - `backup_exclude` destination 163 | - Conflicts for restic-check service 164 | 165 | ## [0.1.0] - 2019-07-23 166 | - First tagged version to allow Arch's AUR to download a tarball archive to install. 167 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2018 Erik Westrup 2 | 3 | Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 4 | 5 | 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 6 | 7 | 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. 8 | 9 | 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. 10 | 11 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 12 | -------------------------------------------------------------------------------- /Library/LaunchAgents/com.github.erikw.restic-backup.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | Label 9 | com.github.erikw.restic-backup 10 | ProgramArguments 11 | 12 | 13 | /bin/bash 14 | -c 15 | source {{ INSTALL_PREFIX }}/etc/restic/default.env.sh && {{ INSTALL_PREFIX }}/bin/restic_backup.sh >>$HOME/$LOG_OUT 2>>$HOME/$LOG_ERR 16 | 17 | EnvironmentVariables 18 | 19 | PATH 20 | {{ INSTALL_PREFIX }}/bin:/usr/local/sbin:/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin 21 | LOG_OUT 22 | /Library/Logs/restic/backup_stdout.log 23 | LOG_ERR 24 | /Library/Logs/restic/backup_stderr.log 25 | 26 | RunAtLoad 27 | 28 | 29 | StartCalendarInterval 30 | 31 | 32 | Hour 33 | 19 34 | Minute 35 | 0 36 | 37 | 38 | 39 | 40 | -------------------------------------------------------------------------------- /Library/LaunchAgents/com.github.erikw.restic-check.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Label 6 | com.github.erikw.restic-check 7 | ProgramArguments 8 | 9 | /bin/bash 10 | -c 11 | source {{ INSTALL_PREFIX }}/etc/restic/default.env.sh && {{ INSTALL_PREFIX }}/bin/restic_check.sh >>$HOME/$LOG_OUT 2>>$HOME/$LOG_ERR 12 | 13 | EnvironmentVariables 14 | 15 | PATH 16 | {{ INSTALL_PREFIX }}/bin:/usr/local/sbin:/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin 17 | LOG_OUT 18 | /Library/Logs/restic/check_stdout.log 19 | LOG_ERR 20 | /Library/Logs/restic/check_stderr.log 21 | 22 | RunAtLoad 23 | 24 | 25 | StartCalendarInterval 26 | 27 | 28 | Day 29 | 1 30 | Hour 31 | 20 32 | Minute 33 | 0 34 | 35 | 36 | 37 | 38 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | #### Notes #################################################################### 2 | # This build process is done in three stages (out-of-source build): 3 | # 1. copy source files to the local build directory. 4 | # 2. build dir: replace the string "{{ INSTALL_PREFIX }}" with the value of $PREFIX 5 | # 3. install files from the build directory to the target directory. 6 | # 7 | # Why this dance? 8 | # * To fully support that a user can install this project to a custom path e.g. 9 | # $(PREFIX=/usr/local make install), we need to modify the files that refer 10 | # to other files on disk. We do this by having a placeholder 11 | # "{{ INSTALL_PREFIX }}" that is substituted with the value of $PREFIX when 12 | # installed. 13 | # * We don't want to modify the files that are controlled by git, thus let's 14 | # copy them to a build directory and then modify. 15 | 16 | #### Non-file targets ######################################################### 17 | .PHONY: help clean uninstall \ 18 | install-systemd install-cron \ 19 | install-targets-script install-targets-conf install-targets-systemd \ 20 | install-targets-cron install-targets-launchagent \ 21 | install-targets-schedtask uninstall-targets-schedtask \ 22 | activate-launchagent-backup deactivate-launchagent-backup \ 23 | activate-launchagent-chec deactivate-launchagent-check 24 | 25 | #### Macros ################################################################### 26 | NOW := $(shell date +%Y-%m-%d_%H:%M:%S) 27 | 28 | # GNU and macOS install have incompatible command line arguments. 29 | GNU_INSTALL := $(shell install --version 2>/dev/null | \ 30 | grep -q GNU && echo true || echo false) 31 | ifeq ($(GNU_INSTALL),true) 32 | BAK_SUFFIX = --suffix=.$(NOW).bak 33 | else 34 | BAK_SUFFIX = -B .$(NOW).bak 35 | endif 36 | 37 | 38 | # Source: https://stackoverflow.com/a/14777895/265508 39 | ifeq ($(OS),Windows_NT) 40 | CUR_OS := Windows 41 | else 42 | CUR_OS := $(shell uname) 43 | endif 44 | 45 | 46 | # Create parent directories of a file, if not existing. 47 | # Reference: https://stackoverflow.com/a/25574592/265508 48 | MKDIR_PARENTS=sh -c '\ 49 | dir=$$(dirname $$1); \ 50 | test -d $$dir || mkdir -p $$dir \ 51 | ' MKDIR_PARENTS 52 | 53 | # LaunchAgent names. 54 | UID := $(shell id -u) 55 | LAUNCHAGENT_BACKUP = com.github.erikw.restic-backup 56 | LAUNCHAGENT_CHECK = com.github.erikw.restic-check 57 | LAUNCHAGENT_TARGET_BACKUP = gui/$(UID)/$(LAUNCHAGENT_BACKUP) 58 | LAUNCHAGENT_TARGET_CHECK = gui/$(UID)/$(LAUNCHAGENT_CHECK) 59 | 60 | # What to substitute {{ INSTALL_PREFIX }} in sources to. 61 | # This can be useful to set to empty on commandline when building e.g. an AUR 62 | # package in a separate build directory (PREFIX). 63 | INSTALL_PREFIX := $(PREFIX) 64 | 65 | # Where to install persistent configuration files. Used by Homebrew. 66 | SYSCONFDIR := $(PREFIX) 67 | 68 | # Where to install LaunchAgent. Used by Homebrew. 69 | LAUNCHAGENTDIR := $(HOME) 70 | 71 | # ScheduledTask powershell scripts. 72 | SCHEDTASK_INSTALL = install.ps1 73 | SCHEDTASK_UNINSTALL = uninstall.ps1 74 | 75 | # Source directories. 76 | DIR_SCRIPT = bin 77 | DIR_CONF = etc/restic 78 | DIR_SYSTEMD = usr/lib/systemd/system 79 | DIR_CRON = etc/cron.d 80 | DIR_LAUNCHAGENT = Library/LaunchAgents 81 | DIR_SCHEDTASK = ScheduledTask 82 | 83 | # Source files. 84 | SRCS_SCRIPT = $(filter-out \ 85 | %cron_mail \ 86 | %systemd-email \ 87 | %nm-unmetered-connection.sh \ 88 | , $(wildcard $(DIR_SCRIPT)/*)) 89 | SRCS_CONF = $(wildcard $(DIR_CONF)/*) 90 | SRCS_SYSTEMD = $(filter-out \ 91 | %status-email-user@.service \ 92 | %nm-unmetered-connection.service \ 93 | , $(wildcard $(DIR_SYSTEMD)/*)) 94 | SRCS_CRON = $(wildcard $(DIR_CRON)/*) 95 | SRCS_LAUNCHAGENT= $(wildcard $(DIR_LAUNCHAGENT)/*) 96 | SRCS_SCHEDTASK = $(wildcard $(DIR_SCHEDTASK)/*) 97 | 98 | # Local build directory. Sources will be copied here, 99 | # modified and then installed from this directory. 100 | BUILD_DIR = build 101 | BUILD_DIR_SCRIPT = $(BUILD_DIR)/$(DIR_SCRIPT) 102 | BUILD_DIR_CONF = $(BUILD_DIR)/$(DIR_CONF) 103 | BUILD_DIR_SYSTEMD = $(BUILD_DIR)/$(DIR_SYSTEMD) 104 | BUILD_DIR_CRON = $(BUILD_DIR)/$(DIR_CRON) 105 | BUILD_DIR_LAUNCHAGENT = $(BUILD_DIR)/$(DIR_LAUNCHAGENT) 106 | BUILD_DIR_SCHEDTASK = $(BUILD_DIR)/$(DIR_SCHEDTASK) 107 | 108 | # Sources copied to build directory. 109 | BUILD_SRCS_SCRIPT = $(addprefix $(BUILD_DIR)/, $(SRCS_SCRIPT)) 110 | BUILD_SRCS_CONF = $(addprefix $(BUILD_DIR)/, $(SRCS_CONF)) 111 | BUILD_SRCS_SYSTEMD = $(addprefix $(BUILD_DIR)/, $(SRCS_SYSTEMD)) 112 | BUILD_SRCS_CRON = $(addprefix $(BUILD_DIR)/, $(SRCS_CRON)) 113 | BUILD_SRCS_LAUNCHAGENT = $(addprefix $(BUILD_DIR)/, $(SRCS_LAUNCHAGENT)) 114 | BUILD_SRCS_SCHEDTASK = $(addprefix $(BUILD_DIR)/, $(SRCS_SCHEDTASK)) 115 | 116 | # Destination directories 117 | DEST_DIR_SCRIPT = $(PREFIX)/$(DIR_SCRIPT) 118 | DEST_DIR_CONF = $(SYSCONFDIR)/$(DIR_CONF) 119 | DEST_DIR_SYSTEMD = $(PREFIX)/$(DIR_SYSTEMD) 120 | DEST_DIR_CRON = $(PREFIX)/$(DIR_CRON) 121 | DEST_DIR_LAUNCHAGENT= $(LAUNCHAGENTDIR)/$(DIR_LAUNCHAGENT) 122 | DEST_DIR_MAC_LOG = $(HOME)/Library/Logs/restic 123 | 124 | # Destination file targets. 125 | DEST_TARGS_SCRIPT = $(addprefix $(PREFIX)/, $(SRCS_SCRIPT)) 126 | DEST_TARGS_CONF = $(addprefix $(SYSCONFDIR)/, $(SRCS_CONF)) 127 | DEST_TARGS_SYSTEMD = $(addprefix $(PREFIX)/, $(SRCS_SYSTEMD)) 128 | DEST_TARGS_CRON = $(addprefix $(PREFIX)/, $(SRCS_CRON)) 129 | DEST_TARGS_LAUNCHAGENT = $(addprefix $(LAUNCHAGENTDIR)/, $(SRCS_LAUNCHAGENT)) 130 | 131 | DEST_LAUNCHAGENT_BACKUP = $(DEST_DIR_LAUNCHAGENT)/$(LAUNCHAGENT_BACKUP).plist 132 | DEST_LAUNCHAGENT_CHECK = $(DEST_DIR_LAUNCHAGENT)/$(LAUNCHAGENT_CHECK).plist 133 | 134 | INSTALLED_FILES = $(DEST_TARGS_SCRIPT) $(DEST_TARGS_CONF) \ 135 | $(DEST_TARGS_SYSTEMD) $(DEST_TARGS_CRON) \ 136 | $(DEST_TARGS_LAUNCHAGENT) 137 | 138 | 139 | #### Targets ################################################################## 140 | # target: help - Default target; displays all targets. 141 | help: 142 | @egrep "#\starget:" [Mm]akefile | cut -d " " -f3- | sort -d 143 | 144 | # target: clean - Remove build files. 145 | clean: 146 | $(RM) -r $(BUILD_DIR) 147 | 148 | # target: uninstall - Uninstall ALL installed (including config) files. 149 | uninstall: uninstall-schedtask 150 | @for file in $(INSTALLED_FILES); do \ 151 | echo $(RM) $$file; \ 152 | $(RM) $$file; \ 153 | done 154 | 155 | # To change the installation root path, 156 | # set the PREFIX variable in your shell's environment, like: 157 | # $ PREFIX=/usr/local make install-systemd 158 | # $ PREFIX=/tmp/test make install-systemd 159 | # target: install-systemd - Install systemd setup. 160 | install-systemd: install-targets-script install-targets-conf \ 161 | install-targets-systemd 162 | 163 | # target: install-cron - Install cron setup. 164 | install-cron: install-targets-script install-targets-conf install-targets-cron 165 | 166 | # target: install-launchagent - Install backup LaunchAgent setup. 167 | install-launchagent: install-targets-script install-targets-conf \ 168 | install-targets-launchagent 169 | 170 | # target: install-launchagent-check - Install check LaunchAgent setup. 171 | # Intended to be run after install-launchagent, thus not requiring scripts/conf 172 | #install-launchagent: install-targets-launchagent 173 | 174 | # target: install-schedtask - Install Windows ScheduledTasks 175 | install-schedtask: install-targets-script install-targets-conf \ 176 | install-targets-schedtask 177 | 178 | # target: uninstall-schedtask - Uninstall Windows ScheduledTasks 179 | uninstall-schedtask: uninstall-targets-schedtask 180 | 181 | # Install targets. Prereq build sources as well, 182 | # so that build dir is re-created if deleted. 183 | install-targets-script: $(DEST_TARGS_SCRIPT) $(BUILD_SRCS_SCRIPT) 184 | install-targets-conf: $(DEST_TARGS_CONF) $(BUILD_SRCS_CONF) 185 | install-targets-systemd: $(DEST_TARGS_SYSTEMD) $(BUILD_SRCS_SYSTEMD) 186 | install-targets-cron: $(DEST_TARGS_CRON) $(BUILD_SRCS_CRON) 187 | install-targets-launchagent: $(DEST_TARGS_LAUNCHAGENT) \ 188 | $(BUILD_SRCS_LAUNCHAGENT) $(DEST_DIR_MAC_LOG) 189 | install-targets-schedtask: $(BUILD_DIR_SCHEDTASK)/$(SCHEDTASK_INSTALL) 190 | test $(CUR_OS) != Windows || ./$< 191 | 192 | uninstall-targets-schedtask: $(BUILD_DIR_SCHEDTASK)/$(SCHEDTASK_UNINSTALL) 193 | test $(CUR_OS) != Windows || ./$< 194 | 195 | # Copies sources to build directory & replace "{{ INSTALL_PREFIX }}". 196 | $(BUILD_DIR)/% : % 197 | @${MKDIR_PARENTS} $@ 198 | cp $< $@ 199 | sed -i.bak -e 's|{{ INSTALL_PREFIX }}|$(INSTALL_PREFIX)|g' $@; rm $@.bak 200 | 201 | # Install destination script files. 202 | $(DEST_DIR_SCRIPT)/%: $(BUILD_DIR_SCRIPT)/% 203 | @${MKDIR_PARENTS} $@ 204 | install -m 0555 $< $@ 205 | 206 | # Install destination conf files. Additionally backup existing files. 207 | $(DEST_DIR_CONF)/%: $(BUILD_DIR_CONF)/% 208 | @${MKDIR_PARENTS} $@ 209 | install -m 0600 -b $(BAK_SUFFIX) $< $@ 210 | 211 | # Install destination systemd files. 212 | $(DEST_DIR_SYSTEMD)/%: $(BUILD_DIR_SYSTEMD)/% 213 | @${MKDIR_PARENTS} $@ 214 | install -m 0644 $< $@ 215 | 216 | # Install destination cron files. 217 | $(DEST_DIR_CRON)/%: $(BUILD_DIR_CRON)/% 218 | @${MKDIR_PARENTS} $@ 219 | install -m 0644 $< $@ 220 | 221 | # Install destination launchagent files. 222 | $(DEST_DIR_LAUNCHAGENT)/%: $(BUILD_DIR_LAUNCHAGENT)/% 223 | @${MKDIR_PARENTS} $@ 224 | install -m 0444 $< $@ 225 | 226 | # Install destination mac log dir. 227 | $(DEST_DIR_MAC_LOG): 228 | mkdir -p $@ 229 | 230 | # target: activate-launchagent-backup - Activate the backup LaunchAgent. 231 | activate-launchagent-backup: 232 | launchctl bootstrap gui/$(UID) $(DEST_LAUNCHAGENT_BACKUP) 233 | launchctl enable $(LAUNCHAGENT_TARGET_BACKUP) 234 | launchctl kickstart -p $(LAUNCHAGENT_TARGET_BACKUP) 235 | 236 | # target: activate-launchagent-check - Activate the check LaunchAgent. 237 | activate-launchagent-check: 238 | launchctl bootstrap gui/$(UID) $(DEST_LAUNCHAGENT_CHECK) 239 | launchctl enable $(LAUNCHAGENT_TARGET_CHECK) 240 | launchctl kickstart -p $(LAUNCHAGENT_TARGET_CHECK) 241 | 242 | # target: deactivate-launchagent-backup - Remove the backup LaunchAgent. 243 | deactivate-launchagent-backup: 244 | launchctl bootout $(LAUNCHAGENT_TARGET_BACKUP) 245 | 246 | # target: deactivate-launchagent-check - Remove the check LaunchAgent. 247 | deactivate-launchagent-check: 248 | launchctl bootout $(LAUNCHAGENT_TARGET_CHECK) 249 | -------------------------------------------------------------------------------- /OSSMETADATA: -------------------------------------------------------------------------------- 1 | osslifecycle=active 2 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Automatic Restic Backups Using Native OS Task Schedulers 2 | *formerly named restic-systemd-automatic-backup* 3 | 4 | [![GitHub Stars](https://img.shields.io/github/stars/erikw/restic-automatic-backup-scheduler?style=social)](#) 5 | [![GitHub Forks](https://img.shields.io/github/forks/erikw/restic-automatic-backup-scheduler?style=social)](#) 6 |
7 | [![Lint](https://github.com/erikw/restic-automatic-backup-scheduler/actions/workflows/linter.yml/badge.svg)](https://github.com/erikw/restic-automatic-backup-scheduler/actions/workflows/linter.yml) 8 | [![Latest tag](https://img.shields.io/github/v/tag/erikw/restic-automatic-backup-scheduler)](https://github.com/erikw/restic-automatic-backup-scheduler/tags) 9 | [![AUR version](https://img.shields.io/aur/version/restic-automatic-backup-scheduler)](https://aur.archlinux.org/packages/restic-automatic-backup-scheduler/) 10 | [![AUR maintainer](https://img.shields.io/aur/maintainer/restic-automatic-backup-scheduler?label=AUR%20maintainer)](https://aur.archlinux.org/packages/restic-automatic-backup-scheduler/) 11 | [![Homebrew Formula](https://img.shields.io/badge/homebrew-erikw%2Ftap-orange)](https://github.com/erikw/homebrew-tap) 12 | [![Open issues](https://img.shields.io/github/issues/erikw/restic-automatic-backup-scheduler)](https://github.com/erikw/restic-automatic-backup-scheduler/issues) 13 | [![Closed issues](https://img.shields.io/github/issues-closed/erikw/restic-automatic-backup-scheduler?color=success)](https://github.com/erikw/restic-automatic-backup-scheduler/issues?q=is%3Aissue+is%3Aclosed) 14 | [![Closed PRs](https://img.shields.io/github/issues-pr-closed/erikw/restic-automatic-backup-scheduler?color=success)](https://github.com/erikw/restic-automatic-backup-scheduler/pulls?q=is%3Apr+is%3Aclosed) 15 | [![License](https://img.shields.io/badge/license-BSD--3-blue)](LICENSE) 16 | [![OSS Lifecycle](https://img.shields.io/osslifecycle/erikw/restic-automatic-backup-scheduler)](https://github.com/Netflix/osstracker) 17 | [![SLOC](https://sloc.xyz/github/erikw/restic-automatic-backup-scheduler?lower=true)](#) 18 | [![Top programming languages used](https://img.shields.io/github/languages/top/erikw/restic-automatic-backup-scheduler)](#) 19 |
20 | 21 | [![Contributors](https://img.shields.io/github/contributors/erikw/restic-automatic-backup-scheduler)](https://github.com/erikw/restic-automatic-backup-scheduler/graphs/contributors) including these top contributors: 22 | 23 | 24 | 25 | 26 |

27 | 28 | Open in GitHub Codespaces 29 |

30 | 31 | # Intro 32 | [restic](https://restic.net/) is a command-line tool for making backups, the right way. Check the official website for a feature explanation. As a storage backend, I recommend [Backblaze B2](https://www.backblaze.com/b2/cloud-storage.html) as restic works well with it, and it is (at the time of writing) very affordable for the hobbyist hacker! (anecdotal: I pay for my full-systems backups each month typically < 1 USD). 33 | 34 | Unfortunately restic does not come pre-configured with a way to run automated backups, say every day. However, it's possible to set this up yourself using built-in tools in your OS and some wrappers. For Linux with systemd, it's convenient to use systemd timers. For macOS systems, we can use built-in LaunchAgents. For Windows we can use ScheduledTasks. Any OS having something cron-like will also work! 35 | 36 | Here follows a step-by step tutorial on how to set it up, with my sample script and configurations that you can modify to suit your needs. 37 | 38 | Note, you can use any restic's supported [storage backends](https://restic.readthedocs.io/en/latest/030_preparing_a_new_repo.html). The setup should be similar, but you will have to use other configuration variables to match your backend of choice. 39 | 40 | ## Project Scope 41 | > [!NOTE] 42 | > **Update:** this project is feature complete (see reasoning below). Only bug fixes will be accepted. Feel free to fork if you want to add more features; being a forking base was the initial scope of this project! 43 | 44 | The scope for this is not to be a full-fledged super solution that solves all the problems and all possible setups. The aim is to be a hackable code base for you to start sewing up the perfect backup solution that fits your requirements! 45 | 46 | Nevertheless, the project should work out of the box, be minimal but still open the doors for configuration and extensions by users. 47 | 48 | To use a different storage backend than B2, you should only need to tweak a few settings variables in the backup profile as well as some restic arguments inside `restic_backup.sh`. 49 | 50 | ## Notes 51 | > [!TIP] 52 | > Navigate this document easily from the Section icon in the top left corner. 53 | README.md sections 54 | 55 | > [!NOTE] 56 | > In the command listing in this document, `$` means a user shell and `#` means a root shell (or use `sudo`). 57 | 58 | 59 | # Requirements 60 | * `restic >=v0.9.6` 61 | * `bash >=v4.0.0` 62 | * (recommended) GNU `make` if you want an automated install 63 | * Arch: part of the `base-devel` meta package, Debian/Ubuntu: part of the `build-essential` meta package, macOS: use the pre-installed or a more recent with Homebrew 64 | 65 | 66 | # Setup 67 | Depending on your system, the setup will look different. Choose one of: 68 | * [Linux + Systemd](#setup-linux-systemd) 69 | * [macOS + LaunchAgent](#setup-macos-launchagent) 70 | * [Windows + ScheduledTask](#setup-windows-scheduledtask) 71 | * [Cron](#setup-cron) - for any system having a cron daemon. Tested on FreeBSD and macOS. 72 | 73 | 74 | ## Setup Linux Systemd 75 | 76 | 77 | > [!NOTE] 78 | > The Linux setup here will assume an installation to `/`. 79 | 80 | Many Linux distributions nowadays use [Systemd](https://en.wikipedia.org/wiki/Systemd), which features good support for running services and scheduled jobs. If your distribution is no on Systemd, check out the [cron setup](#setup-cron) instead. 81 | 82 | **TL;DR setup** 83 | 1. [Create](#1-create-backblaze-b2-account-bucket-and-keys) B2 bucket + credentials 84 | 1. Install scripts, configs systemd units/timers: 85 | * With `make`: 86 | ```console 87 | $ sudo make install-systemd 88 | ``` 89 | * Arch Linux users: use the [AUR](https://aur.archlinux.org/packages/restic-automatic-backup-scheduler) package, e.g. 90 | ```console 91 | $ yay -S restic-automatic-backup-scheduler 92 | ``` 93 | 1. Fill out [configuration values](#2-configure-b2-credentials-locally) in `/etc/restic`. 94 | 1. [Initialize](#3-initialize-remote-repo) the remote repo. 95 | Source the profile to make all needed configuration available to `restic(1)`. All commands after this assumes the profile is sourced in the current shell. 96 | ```console 97 | # source /etc/restic/default.env.sh 98 | # restic init 99 | ``` 100 | 1. Configure [how often](https://www.freedesktop.org/software/systemd/man/systemd.time.html#Calendar%20Events) backups should be done. 101 | * If needed, edit `OnCalendar` in `/usr/lib/systemd/system/restic-backup@.timer`. 102 | 1. Enable automated backup for starting with the system & make the first backup: 103 | ```console 104 | # systemctl enable --now restic-backup@default.timer 105 | ``` 106 | 1. Watch the first backup progress with Systemd journal: 107 | ```console 108 | # journalctl -f --lines=50 -u restic-backup@default 109 | ``` 110 | 1. Verify the backup 111 | ```console 112 | # restic snapshots 113 | ``` 114 | 1. (recommended) Enable the check job that verifies that the backups for the profile are all intact. 115 | ```console 116 | # systemctl enable --now restic-check@default.timer 117 | ```` 118 | 1. (optional) Define multiple profiles: just make a copy of the `default.env.sh` and use the defined profile name in place of `default` to run backups or enable timers. Notice that the value after `@` works as a parameter. 119 | ```console 120 | # systemctl enable restic-backup@other_profile.timer 121 | ``` 122 | 1. Consider more [optional features](#optional-features). 123 | 124 | 125 | ## Setup macOS LaunchAgent 126 | 127 | 128 | > [!NOTE] 129 | > The macOS setup here will assume a Homebrew installation to the [recommended default location](https://docs.brew.sh/FAQ#why-should-i-install-homebrew-in-the-default-location). This is [`$HOMEBREW_PREFIX` (`brew --prefix`)](https://docs.brew.sh/Formula-Cookbook#variables-for-directory-locations) , which is `/usr/local` on Intel Macs and `/opt/homebrew` on [Apple Silicon](https://docs.brew.sh/FAQ#why-is-the-default-installation-prefix-opthomebrew-on-apple-silicon). 130 | 131 | [Launchd](https://www.launchd.info/) is the modern built-in service scheduler in macOS. It has support for running services as root (Daemon) or as a normal user (Agent). Here we set up a LaunchAgent to be run as your normal user for starting regular backups. 132 | 133 | **TL;DR setup** 134 | 1. [Create](#1-create-backblaze-b2-account-bucket-and-keys) B2 bucket + credentials 135 | 1. Install scripts, configs and LaunchAgent: 136 | * (recommended) with Homebrew from the [erikw/homebrew-tap](https://github.com/erikw/homebrew-tap): 137 | ```console 138 | $ brew install erikw/tap/restic-automatic-backup-scheduler 139 | ``` 140 | * Using `make`: 141 | ```console 142 | $ make PREFIX=$(brew --prefix) install-launchagent 143 | ``` 144 | 1. Fill out [configuration values](#2-configure-b2-credentials-locally) in `$(brew --prefix)/etc/restic`. 145 | 1. [Initialize](#3-initialize-remote-repo) the remote repo. 146 | Source the profile to make all needed configuration available to `restic(1)`. All commands after this assumes the profile is sourced in the current shell. 147 | ```console 148 | $ source $(brew --prefix)/etc/restic/default.env.sh 149 | $ restic init 150 | ``` 151 | 1. Configure [how often](https://developer.apple.com/library/archive/documentation/MacOSX/Conceptual/BPSystemStartup/Chapters/ScheduledJobs.html#//apple_ref/doc/uid/10000172i-CH1-SW1) backups should be done. If needed, edit `OnCalendar` in 152 | * Homebrew install: `~/Library/LaunchAgents/homebrew.mxcl.restic-automatic-backup-scheduler.plist`. 153 | * Note that with Homebrew install, this file will only be available after running the `$ brew services start [...]` command in the next step. Run that command and come back here. 154 | * `make` install: `~/Library/LaunchAgents/com.github.erikw.restic-backup.plist`. 155 | 1. Enable automated backup for starting with the system & make the first backup: 156 | * Homebrew install: 157 | ```console 158 | $ brew services start restic-automatic-backup-scheduler 159 | ``` 160 | * `make` install: 161 | ```console 162 | $ launchctl bootstrap gui/$UID ~/Library/LaunchAgents/com.github.erikw.restic-backup.plist 163 | $ launchctl enable gui/$UID/com.github.erikw.restic-backup 164 | $ launchctl kickstart -p gui/$UID/com.github.erikw.restic-backup 165 | ``` 166 | As a convenience, a shortcut for the above commands are `$ make activate-launchagent-backup`. 167 | 1. Watch the first backup progress from the log files: 168 | ```console 169 | $ tail -f ~/Library/Logs/restic/backup* 170 | ``` 171 | 1. Verify the backup 172 | ```console 173 | $ restic snapshots 174 | ``` 175 | 1. (recommended) Enable the check job that verifies that the backups for the profile are all intact. 176 | * Homebrew install: 177 | ```console 178 | $ brew services start restic-automatic-backup-scheduler-check 179 | ``` 180 | * `make` install: 181 | ```console 182 | $ launchctl bootstrap gui/$UID ~/Library/LaunchAgents/com.github.erikw.restic-check.plist 183 | $ launchctl enable gui/$UID/com.github.erikw.restic-check 184 | $ launchctl kickstart -p gui/$UID/com.github.erikw.restic-check 185 | ``` 186 | As a convenience, a shortcut for the above commands are `$ make activate-launchagent-check`. 187 | 1. Consider more [optional features](#optional-features). 188 | 189 | ### Homebrew Setup Notes 190 | Then control the service with homebrew: 191 | ```console 192 | $ brew services start restic-automatic-backup-scheduler 193 | $ brew services restart restic-automatic-backup-scheduler 194 | $ brew services stop restic-automatic-backup-scheduler 195 | ``` 196 | 197 | If `services start` fails, it might be due to previous version installed. In that case remove the existing version and try again: 198 | ```console 199 | $ launchctl bootout gui/$UID/com.github.erikw.restic-backup 200 | $ brew services start restic-automatic-backup-scheduler 201 | ``` 202 | 203 | ### Make Setup Notes 204 | Use the `disable` command to temporarily pause the agent, or `bootout` to uninstall it. 205 | ``` 206 | $ launchctl disable gui/$UID/com.github.erikw.restic-backup 207 | $ launchctl bootout gui/$UID/com.github.erikw.restic-backup 208 | ``` 209 | 210 | If you updated the `.plist` file, you need to issue the `bootout` followed by `bootrstrap` and `enable` sub-commands of `launchctl`. This will guarantee that the file is properly reloaded. 211 | 212 | 213 | 214 | ## Setup Windows ScheduledTask 215 | 216 | 217 | Windows comes with a built-in task scheduler called [ScheduledTask](https://docs.microsoft.com/en-us/powershell/module/scheduledtasks/new-scheduledtask?view=windowsserver2022-ps). The frontend app is "Task Scheduler" (`taskschd.msc`) and we can use PowerShell commands to install a new scheduled task. 218 | 219 | I describe here one of may ways you can get restic and this backup script working on Windows. Here I chose to work with `scoop` and `git-bash`. 220 | 221 | 222 | **TL;DR setup** 223 | 1. Install [scoop](https://scoop.sh/) 224 | 1. Install dependencies from a PowerShell with *administrator privileges*. `pwsh` should be installed to be able to run powershell in shebang scripts. 225 | ```console 226 | powershell> scoop install restic make git pwsh 227 | ``` 228 | 1. In a *non-privileged* PowerShell, start git-bash and clone this repo 229 | ```console 230 | powershell> git-bash 231 | git-bash$ mkdir ~/src && cd ~/src/ 232 | git-bash$ git clone https://github.com/erikw/restic-automatic-backup-scheduler.git && cd $(basename "$_" .git) 233 | ``` 234 | 1. Install scripts, configs and ScheduledTasks 235 | ```console 236 | git-bash$ make install-schedtask 237 | ``` 238 | 1. Fill out [configuration values](#2-configure-b2-credentials-locally) in `/etc/restic`. 239 | ```console 240 | git-bash$ vim /etc/restic/* 241 | ``` 242 | Note that you should use cygwin/git-bash paths. E.g. in `default.env.sh` you could have 243 | ```bash 244 | export RESTIC_BACKUP_PATHS='/c/Users//My Documents' 245 | ``` 246 | 1. [Initialize](#3-initialize-remote-repo) the remote repo. 247 | Source the profile to make all needed configuration available to `restic(1)`. All commands after this assumes the profile is sourced in the current shell. 248 | ```console 249 | git-bash$ source /etc/restic/default.env.sh 250 | git-bash$ restic init 251 | ``` 252 | 1. Make the first backup 253 | ```console 254 | git-bash$ restic_backup.sh 255 | ``` 256 | 1. Verify the backup 257 | ```console 258 | git-bash$ restic snapshots 259 | ``` 260 | 1. Inspect the installed ScheduledTasks and make a test run 261 | 1. Open the app "Task Scheduler" (`taskschd.msc`) 262 | 1. Go to the local "Task Scheduler Library" 263 | 1. Right click on one of the newly installed tasks e.g. `restic_backup` and click "run". 264 | - If the tasks are not there, maybe you opended it up before `make install-schedtask`: just close and start it again to refresh. 265 | 1. Now a git-bash window should open running `restic_backup.sh`, and the next time the configured schedule hits! 266 | 1. Consider more [optional features](#optional-features). 267 | 268 | 269 | With `taskschd.msc` you can easily start, stop, delete and configure the scheduled tasks to your liking: 270 | Windows Task Schedulder 271 | 272 | 273 | ## Setup Cron 274 | 275 | 276 | > [!NOTE] 277 | > There are many different cron [implementations](https://wiki.archlinux.org/title/Cron) out there and they all work slightly different. 278 | 279 | Any system that has a cron-like system can easily setup restic backups as well. However if you system supports any of the previous setups, those are recommended over cron as they provide more features and reliability for your backups. 280 | 281 | 282 | 283 | **TL;DR setup** 284 | 1. [Create](#1-create-backblaze-b2-account-bucket-and-keys) B2 bucket + credentials 285 | 1. Install scripts, configs systemd units/timers: 286 | ```console 287 | $ sudo make install-cron 288 | ``` 289 | * This assumes that your cron supports dropping files into `/etc/cron.d/`. If that is not the case, simply copy the relevant contents of the installed `/etc/cron.d/restic` in to your `/etc/crontab`. 290 | ```console 291 | # grep "^@.*restic_" /etc/cron.d/restic >> /etc/crontab 292 | ``` 293 | 1. Fill out [configuration values](#2-configure-b2-credentials-locally) in `/etc/restic`. 294 | 1. [Initialize](#3-initialize-remote-repo) the remote repo. 295 | Source the profile to make all needed configuration available to `restic(1)`. All commands after this assumes the profile is sourced in the current shell. 296 | ```console 297 | # source /etc/restic/default.env.sh 298 | # restic init 299 | ``` 300 | 1. Make the first backup 301 | ```console 302 | # restic_backup.sh 303 | ``` 304 | 1. Verify the backup 305 | ```console 306 | # restic snapshots 307 | ``` 308 | 1. Configure [how often](https://crontab.guru/) backups should be done by directly editing `/etc/cron.d/restic` (or `/etc/crontab`). 309 | 1. Consider more [optional features](#optional-features). 310 | 311 | 312 | ## Detailed Manual Setup 313 | 314 | 315 | This is a more detailed explanation than the TL;DR sections above that will give you more understanding in the setup. This section is more general, but uses Linux + Systemd as the example setup. 316 | 317 | #### 0. Clone Repo 318 | ```console 319 | $ git clone https://github.com/erikw/restic-automatic-backup-scheduler.git && cd $(basename "$_" .git) 320 | ```` 321 | 322 | Make a quick search-and-replace in the source files: 323 | ```console 324 | $ find bin etc usr Library ScheduledTask -type f -exec sed -i.bak -e 's|{{ INSTALL_PREFIX }}||g' {} \; -exec rm {}.bak \; 325 | ``` 326 | and you should now see that all files have been changed like e.g. 327 | ```diff 328 | -export RESTIC_PASSWORD_FILE="{{ INSTALL_PREFIX }}/etc/restic/pw.txt" 329 | +export RESTIC_PASSWORD_FILE="/etc/restic/pw.txt" 330 | ``` 331 | 332 | Why? The OS specific TL;DR setups above all use the [Makefile](Makefile) or a package manager to install these files. The placeholder string `{{ INSTALL_PREFIX }}` is in the source files for portability reasons, so that the Makefile can support all different operating systems. `make` users can set a different `$PREFIX` when installing like `PREFIX=/usr/local make install-systemd`. 333 | 334 | In this detailed manual setup we will copy all files manually to `/etc`and `/bin`. Thus, we need to remove the placeholder string `{{ INSTALL_PREFIX }}` in the source files as a first step. 335 | 336 | 337 | #### 1. Create Backblaze B2 Account, Bucket and Keys 338 | In short: 339 | 1. Create a [Backblaze](https://www.backblaze.com/) account (use 2FA!). 340 | 1. Create a new [B2 bucket](https://secure.backblaze.com/b2_buckets.htm). 341 | * Private, without B2 encryption and without the object lock feature 342 | 1. Create a pair of [keyId and applicationKey](https://secure.backblaze.com/app_keys.htm?bznetid=17953438771644852981527) 343 | * Limit scope of the new id and key pair to only the above created bucket. 344 | 345 | First, see this official Backblaze [tutorial](https://help.backblaze.com/hc/en-us/articles/4403944998811-Quickstart-Guide-for-Restic-and-Backblaze-B2-Cloud-Storage) on restic, and follow the instructions ("Create Backblaze account with B2 enabled") there on how to create a new B2 bucket. In general, you'd want a private bucket, without B2 encryption (restic does the encryption client side for us) and without the object lock feature. 346 | 347 | For restic to be able to connect to your bucket, you want to in the B2 settings create a pair of keyID and applicationKey. It's a good idea to create a separate pair of ID and Key with for each bucket that you will use, with limited read&write access to only that bucket. 348 | 349 | 350 | #### 2. Configure B2 Credentials Locally 351 | Put these files in `/etc/restic/`: 352 | * `_global.env.sh`: Fill this file out with your global settings including B2 keyID & applicationKey. 353 | * `default.env.sh`: This is the default profile. Fill this out with bucket name, backup paths and retention policy. This file sources `_global.env.sh` and is thus self-contained and can be sourced in the shell when you want to issue some manual restic commands. For example: 354 | ```console 355 | $ source /etc/restic/default.env.sh 356 | $ restic snapshots # You don't have to supply all parameters like --repo, as they are now in your environment! 357 | ```` 358 | * `pw.txt`: This file should contain the restic password (single line) used to encrypt the repository. This is a new password what soon will be used when initializing the new repository. It should be unique to this restic backup repository and is needed for restoring from it. Don't re-use your B2 login password, this should be different. For example you can generate a 128 character password (must all be on one line) with: 359 | ```console 360 | $ openssl rand -base64 128 | tr -d '\n' > /etc/restic/pw.txt 361 | ``` 362 | * `backup_exclude.txt`: List of file patterns to ignore. This will trim down your backup size and the speed of the backup a lot when done properly! 363 | 364 | #### 3. Initialize remote repo 365 | Now we must initialize the repository on the remote end: 366 | ```console 367 | $ sudo -i 368 | # source /etc/restic/default.env.sh 369 | # restic init 370 | ``` 371 | 372 | #### 4. Script for doing the backup 373 | Put this file in `/bin`: 374 | * `restic_backup.sh`: A script that defines how to run the backup. The intention is that you should not need to edit this script yourself, but be able to control everything from the `*.env.sh` profiles. 375 | 376 | Restic support exclude files. They list file pattern paths to exclude from you backups, files that just occupy storage space, backup-time, network and money. `restic_backup.sh` allows for a few different exclude files. 377 | * `/etc/restic/backup_exclude.txt` - global exclude list. You can use only this one if your setup is easy. This is set in `_global.env.sh`. If you need a different file for another profile, you can override the envvar `RESTIC_BACKUP_EXCLUDE_FILE` in this profile. 378 | * `.backup_exclude.txt` per backup path. If you have e.g. an USB disk mounted at /mnt/media and this path is included in the `$RESTIC_BACKUP_PATHS`, you can place a file `/mnt/media/.backup_exclude.txt` and it will automatically picked up. The nice thing about this is that the backup paths are self-contained in terms of what they shoud exclude! 379 | 380 | #### 5. Make first backup 381 | Now see if the backup itself works, by running as root 382 | 383 | ```console 384 | # source /etc/restic/default.env.sh 385 | # /bin/restic_backup.sh 386 | ```` 387 | 388 | #### 6. Verify the backup 389 | As the `default.env.sh` is already sourced in your root shell, you can now just list the snapshost 390 | ```console 391 | # restic snapshots 392 | ``` 393 | 394 | Alternatively you can mount the restic snapshots to a directory set `/mnt/restic` 395 | ```console 396 | # restic mount /mnt/restic 397 | # ls /mnt/restic 398 | ``` 399 | 400 | #### 7. Backup automatically 401 | All OS setups differs in what task scheduler they use. As a demonstration, let's look at how we can do this with systemd under Linux here. 402 | 403 | Put these files in `/etc/systemd/system` (note that the Makefile installs as package to `/usr/lib/systemd/system`) 404 | * `restic-backup@.service`: A service that calls the backup script with the specified profile. The profile is specified 405 | by the value after `@` when running it (see below). 406 | * `restic-backup@.timer`: A timer that starts the former backup every day (same thing about profile here). 407 | * If needed, edit this file to configure [how often](https://www.freedesktop.org/software/systemd/man/systemd.time.html#Calendar%20Events) back up should be made. See the `OnCalendar` key in the file. 408 | 409 | Now simply enable the timer with: 410 | ```console 411 | # systemctl enable --now restic-backup@default.timer 412 | ```` 413 | 414 | You can see when your next backup is scheduled to run with 415 | ```console 416 | # systemctl list-timers | grep restic 417 | ``` 418 | 419 | and see the status of a currently running backup with: 420 | ```console 421 | # systemctl status restic-backup 422 | ``` 423 | 424 | or start a backup manually: 425 | ```console 426 | $ systemctl start restic-backup@default 427 | ``` 428 | 429 | You can follow the backup stdout output live as backup is running with: 430 | ```console 431 | $ journalctl -f -u restic-backup@default.service 432 | ```` 433 | (skip `-f` to see all backups that has run) 434 | 435 | 436 | #### Recommended: Automated Backup Checks 437 | Once in a while it can be good to do a health check of the remote repository, to make sure it's not getting corrupt. This can be done with `$ restic check`. 438 | 439 | There is companion scripts, service and timer (`*check*`) to restic-backup.sh that checks the restic backup for errors; look in the repo in `usr/lib/systemd/system/` and `bin/` and copy what you need over to their corresponding locations. 440 | 441 | ```console 442 | # systemctl enable --now restic-check@default.timer 443 | ```` 444 | 445 | 446 | ## Optional Features 447 | 448 | 449 | ### Optional: Multiple profiles 450 | To have different backup jobs having e.g. different buckets, backup path of schedule, just make a copy of the `default.env.sh` and use the defined profile name in place of `default` in the previous steps. 451 | 452 | To create a different backup and use you can do: 453 | ```console 454 | # cp /etc/restic/default.env.sh /etc/restic/other.env.sh 455 | # vim /etc/restic/other.env.sh # Set backup path, bucket etc. 456 | # source /etc/restic/other.env.sh 457 | # restic_backup.sh 458 | ``` 459 | 460 | ### Optional: Summary stats log 461 | 462 | When enabled, it will write to a CSV log file the stats after each backup. Can be enabled by uncommenting its env variable (`RESTIC_BACKUP_STATS_DIR`) on the global environment file or defining it on a specific profile. 463 | 464 | The stats log (as well as) the desktop notifications incur in an additional run of `restic snapshots` and `restic diff`. This execution is shared with the notifications (no extra run). 465 | 466 | ### Optional: Desktop Notifications 467 | 468 | 469 | It's a good idea to be on top of your backups to make sure that they don't increase a lot in size and incur high costs. However, it's notoriously tricky to make GUI notifications correctly from a non-user process (e.g. root). 470 | 471 | Therefore, this project provides a lightweight solution for desktop notifications that works like this: Basically `restic_backup.sh` will append a summary line of the last backup to a user-owned file (the user running your OS's desktop environment) in a fire-and-forget fashion. Then the user has a process that reads this and forward each line as a new message to the desktop environment in use. 472 | 473 | To set desktop notifications up: 474 | 1. Create a special FIFO file as your desktop user: 475 | ```console 476 | $ mkfifo /home/user/.cache/notification-queue 477 | ``` 478 | 1. In your profile, e.g. `/etc/restic/default.sh`, set: 479 | ```bash 480 | RESTIC_BACKUP_NOTIFICATION_FILE=/home/user/.cache/notification-queue 481 | ``` 482 | 1. Create a listener on the notification queue file that forwards to desktop notifications 483 | * Linux auto start + cross-platform notifier / notify-send 484 | * [notification-queue-notifier](https://github.com/gerardbosch/dotfiles/blob/2130d54daa827e7f885abac0d4f10b6f67d28ad3/home/bin/notification-queue-notifier) 485 | * [notification-queue.desktop](https://github.com/gerardbosch/dotfiles-linux/blob/ea0f75bfd7a356945544ecaa42a2fc35c9fab3a1/home/.config/autostart/notification-queue.desktop) 486 | * macOS auto start + [terminal-notifier](https://github.com/julienXX/terminal-notifier) 487 | * [notification-queue-notifier.sh](https://github.com/erikw/dotfiles/blob/8a942defe268292200b614951cdf433ddccf7170/bin/notification-queue-notifier.sh) 488 | * [com.user.notificationqueue.plist](https://github.com/erikw/dotfiles/blob/8a942defe268292200b614951cdf433ddccf7170/.config/LaunchAgents/com.user.notificationqueue.plist) 489 | 490 | 491 | 492 | ### Optional: Email Notification on Failure 493 | #### Systemd 494 | We want to be aware when the automatic backup fails, so we can fix it. Since my laptop does not run a mail server, I went for a solution to set up my laptop to be able to send emails with [postfix via my Gmail](https://easyengine.io/tutorials/linux/ubuntu-postfix-gmail-smtp/). Follow the instructions over there. 495 | 496 | Put this file in `/bin`: 497 | * `systemd-email`: Sends email using sendmail(1). This script also features time-out for not spamming Gmail servers and getting my account blocked. 498 | 499 | Put this file in `/etc/systemd/system/`: 500 | * `status-email-user@.service`: A service that can notify you via email when a systemd service fails. Edit the target email address in this file, and replace or remove `{{ INSTALL_PREFIX }}` according to your installation. 501 | 502 | Now edit `/usr/lib/systemd/system/restic-backup@.service` and `/usr/lib/systemd/system/restic-check@.service` to call this service failure. 503 | ``` 504 | OnFailure=status-email-user@%n.service 505 | ``` 506 | #### Cron 507 | Use `bin/cron_mail`: A wrapper for running cron jobs, that sends output of the job as an email using the mail(1) command. This assumes that the `mail` program is correctly setup on the system to send emails. 508 | 509 | To use this, wrap the restic script command with it in your cron file like: 510 | ```diff 511 | -@midnight root . /etc/restic/default.sh && restic_backup.sh 512 | +@midnight root . /etc/restic/default.sh && cron_mail restic_backup.sh 513 | ``` 514 | 515 | 516 | ### Optional: No Backup on Metered Connections (Linux/systemd only) 517 | For a laptop, it can make sense to not do heavy backups when your on a metered connection like a shared connection from you mobile phone. To solve this we can set up a systemd service that is in success state only when a connection is unmetered. Then we can tell our backup service to depend on this service simply! When the unmetered service detects an unmetered connection it will go to failed state. Then our backup service will not run as it requires this other service to be in success state. 518 | 519 | 1. Edit `restic-backup@.service` and `restic-check@.service` to require the new service to be in success state: 520 | ``` 521 | Requires=nm-unmetered-connection.service 522 | After=nm-unmetered-connection.service 523 | ``` 524 | 1. Copy and paste the command below, it will install the following files and refresh systemd daemon: 525 | 1. Put this file in `/etc/systemd/system/`: 526 | * `nm-unmetered-connection.service`: A service that is in success state only if the connection is unmetered. 527 | 1. Install this file in `/bin`: 528 | * `nm-unmetered-connection.sh`: Detects metered connections and returns an error code if one is detected. This scripts requires the Gnome [NetworkManager](https://wiki.gnome.org/Projects/NetworkManager) to be installed (modify this script if your system has a different network manager). 529 | 1. Reload systemd with 530 | ```console 531 | # systemctl daemon-reload 532 | ``` 533 | > [!TIP] 534 | > All steps but the first can be done in one go if you use the Makefile. Set `$PREFIX` as needed or leave empty for install to `/`. 535 | 536 | 537 | ```bash 538 | sudo bash -c 'export PREFIX= 539 | make build/usr/lib/systemd/system/nm-unmetered-connection.service 540 | install -m 0644 build/usr/lib/systemd/system/nm-unmetered-connection.service $PREFIX/etc/systemd/system 541 | install -m 0555 bin/nm-unmetered-connection.sh /bin 542 | systemctl daemon-reload 543 | ' 544 | ``` 545 | 546 | ### Optional: Restic Wrapper Script 547 | For convenience there's a `restic` wrapper script that makes loading profiles and **running restic** 548 | straightforward (it needs to run with sudo to read environment). Just run: 549 | 550 | * `sudo resticw WHATEVER` (e.g. `sudo resticw snapshots`) to use the default profile. 551 | * You can run the wrapper by passing a specific profile: `resticw -p anotherprofile snapshots`. 552 | * The wrapper has extras on top of `restic` like `--diff-latest` option. 553 | 554 | Useful commands: 555 | | Command | Description | 556 | |---------------------------------------------------|---------------------------------------------------------------------------------------| 557 | | `resticw snapshots` | List backup snapshots | 558 | | `resticw diff ` | Show the changes between backup snapshots | 559 | | `resticw stats` / `resticw stats snapshotId ...` | Show the statistics for the whole repo or the specified snapshots | 560 | | `resticw mount /mnt/restic` | Mount your remote repository | 561 | | `resticw --diff-latest` | Show latest snapshot changes: Runs `restic diff` after finding the latest 2 snapshots | 562 | 563 | 564 | 565 | # Uninstall 566 | There is a make target to remove all files (scripts and **configs)** that were installed by `sudo make install-*`. Just run: 567 | 568 | ```console 569 | $ sudo make uninstall 570 | ``` 571 | 572 | 573 | # Debugging 574 | The best way to debug what's going on is to run the `restic_backup.sh` script with bash's trace function. You can activate it by running the script with `bash -x`: 575 | 576 | ```consle 577 | $ source /etc/restic/default.env.sh 578 | $ bash -x /bin/restic_backup.sh 579 | ``` 580 | 581 | To debug smaller portions of the backup script, insert these lines at the top and bottom of the relevant code portions e.g.: 582 | 583 | ```bash 584 | set -x 585 | exec 2>/tmp/restic-automatic-backup-scheduler.log 586 | 587 | set +x 588 | ``` 589 | 590 | and then inspect the outputs like 591 | 592 | ```shell 593 | $ less /tmp/restic-automatic-backup-scheduler.log 594 | $ tail -f /tmp/restic-automatic-backup-scheduler.log # or follow output like this. 595 | ``` 596 | 597 | 598 | # Development 599 | * To not mess up your real installation when changing the `Makefile` simply install to a `$PREFIX` like 600 | ```console 601 | $ PREFIX=/tmp/restic-test make install-systemd 602 | ``` 603 | * **Updating the `resticw` parser:** If you ever update the usage `DOC`, you will need to refresh the auto-generated parser: 604 | ```console 605 | $ pip install doctopt.sh 606 | $ doctopt.sh usr/local/bin/resticw 607 | ``` 608 | 609 | # Releasing 610 | 1. Create a new version of this project by using [semver-cli](https://github.com/maykonlsf/semver-cli). 611 | ```shell 612 | vi CHANGELOG.md 613 | semver up minor 614 | ver=$(semver get release) 615 | git commit -am "Bump version to $ver" && git tag $ver && git push --atomic origin main $ver 616 | ``` 617 | 1. Update version in the AUR [PKGBUILD](https://aur.archlinux.org/packages/restic-automatic-backup-scheduler/) 618 | 1. Update version in the Homebrew Formulas (see the repo README): 619 | * [restic-automatic-backup-scheduler](https://github.com/erikw/homebrew-tap/blob/main/Formula/restic-automatic-backup-scheduler.rb) 620 | * [restic-automatic-backup-scheduler-check](https://github.com/erikw/homebrew-tap/blob/main/Formula/restic-automatic-backup-scheduler-check.rb) 621 | -------------------------------------------------------------------------------- /ScheduledTask/install.ps1: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env pwsh 2 | # Install restic scheduled tasks. 3 | # Test run the installed actions by 4 | # 1. open the app "Task Scheduler" (taskschd.msc) 5 | # 2. go to the local "Task Scheduler Library" 6 | # 3. right click on the new tasks and click "run". 7 | # Reference: https://blogs.technet.microsoft.com/heyscriptingguy/2015/01/13/use-powershell-to-create-scheduled-tasks/ 8 | # Reference: https://www.davidjnice.com/cygwin_scheduled_tasks.html 9 | 10 | 11 | # Install restic_backup.sh 12 | $action = New-ScheduledTaskAction -Execute "$(scoop prefix git)\git-bash.exe" -Argument '-l -c "source {{ INSTALL_PREFIX }}/etc/restic/default.env.sh && {{ INSTALL_PREFIX }}/bin/restic_backup.sh"' 13 | $trigger = New-ScheduledTaskTrigger -Daily -At 7pm 14 | Register-ScheduledTask -Action $action -Trigger $trigger -TaskName "restic_backup" -Description "Daily backup to B2 with restic." 15 | 16 | # Install restic_check.sh 17 | $action = New-ScheduledTaskAction -Execute "$(scoop prefix git)\git-bash.exe" -Argument '-l -c "source {{ INSTALL_PREFIX }}/etc/restic/default.env.sh && {{ INSTALL_PREFIX }}/bin/restic_check.sh"' 18 | $trigger = New-ScheduledTaskTrigger -Weekly -WeeksInterval 4 -DaysOfWeek Sunday -At 8pm -RandomDelay 128 19 | Register-ScheduledTask -Action $action -Trigger $trigger -TaskName "restic_check" -Description "Check B2 backups with restic." 20 | -------------------------------------------------------------------------------- /ScheduledTask/uninstall.ps1: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env pwsh 2 | # Uninstall restic scheduled tasks. 3 | 4 | Unregister-ScheduledTask -TaskName "restic_backup" -Confirm:$false 5 | Unregister-ScheduledTask -TaskName "restic_check" -Confirm:$false 6 | -------------------------------------------------------------------------------- /bin/cron_mail: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | # vi: ft=sh 3 | # 4 | # To be called by a cron job as a wrapper that sends stdour and stderr via the mail program. 5 | # Why? Because of FreeBSD the system cron uses sendmail, and I want to use ssmtp. 6 | # Make your crontab files like: 7 | #SHELL=/bin/sh 8 | #PATH=/etc:/bin:/sbin:/usr/bin:/usr/sbin:/usr/local/sbin:{{ INSTALL_PREFIX }}/bin 9 | #@daily root cron_mail freebsd-update cron 10 | 11 | mail_target=root 12 | scriptname=${0##*/} 13 | 14 | if [ $# -eq 0 ]; then 15 | echo "No program to run given!" >&2 16 | exit 1 17 | fi 18 | cmd="$*" 19 | 20 | body=$(eval "$cmd" 2>&1) 21 | 22 | if [ -n "$body" ];then 23 | echo "$body" | mail -s "${scriptname}: ${cmd}" $mail_target 24 | fi 25 | -------------------------------------------------------------------------------- /bin/nm-unmetered-connection.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | # Requires Gnome NetworkManager 3 | 4 | systemctl is-active dbus.service >/dev/null 2>&1 || exit 0 5 | systemctl is-active NetworkManager.service >/dev/null 2>&1 || exit 0 6 | 7 | metered_status=$(dbus-send --system --print-reply=literal \ 8 | --system --dest=org.freedesktop.NetworkManager \ 9 | /org/freedesktop/NetworkManager \ 10 | org.freedesktop.DBus.Properties.Get \ 11 | string:org.freedesktop.NetworkManager string:Metered \ 12 | | grep -o ".$") 13 | 14 | if [[ $metered_status =~ (1|3) ]]; then 15 | echo Current connection is metered 16 | exit 1 17 | else 18 | exit 0 19 | fi 20 | -------------------------------------------------------------------------------- /bin/restic_backup.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | # Make a backup with restic to Backblaze B2. 3 | # 4 | # This script is typically run (as root user) either like: 5 | # - from restic service/timer: $PREFIX/etc/systemd/system/restic-backup.{service,timer} 6 | # - from a cronjob: $PREFIX/etc/cron.d/restic 7 | # - manually by a user. For it to work, the environment variables must be set in the shell where this script is executed 8 | # $ source $PREFIX/etc/default.env.sh 9 | # $ restic_backup.sh 10 | 11 | set -o errexit 12 | set -o pipefail 13 | [[ "${TRACE-0}" =~ ^1|t|y|true|yes$ ]] && set -o xtrace 14 | 15 | # Clean up lock if we are killed. 16 | # If killed by systemd, like $(systemctl stop restic), then it kills the whole cgroup and all it's subprocesses. 17 | # However if we kill this script ourselves, we need this trap that kills all subprocesses manually. 18 | exit_hook() { 19 | echo "In exit_hook(), being killed" >&2 20 | jobs -p | xargs kill 21 | restic unlock 22 | } 23 | trap exit_hook INT TERM 24 | 25 | 26 | # Assert that all needed environment variables are set. 27 | # TODO in future if this grows, move this to a restic_lib.sh 28 | assert_envvars() { 29 | local varnames=("$@") 30 | for varname in "${varnames[@]}"; do 31 | if [ -z ${!varname+x} ]; then 32 | printf "%s must be set for this script to work.\n\nDid you forget to source a {{ INSTALL_PREFIX }}/etc/restic/*.env.sh profile in the current shell before executing this script?\n" "$varname" >&2 33 | exit 1 34 | fi 35 | done 36 | } 37 | 38 | warn_on_missing_envvars() { 39 | local unset_envs=() 40 | local varnames=("$@") 41 | for varname in "${varnames[@]}"; do 42 | if [ -z "${!varname-}" ]; then 43 | unset_envs=("${unset_envs[@]}" "$varname") 44 | fi 45 | done 46 | 47 | if [ ${#unset_envs[@]} -gt 0 ]; then 48 | printf "The following env variables are recommended, but have not been set. This script may not work as expected: %s\n" "${unset_envs[*]}" >&2 49 | fi 50 | } 51 | 52 | # Log the backup summary stats to a CSV file 53 | logBackupStatsCsv() { 54 | local snapId="$1" added="$2" removed="$3" snapSize="$4" 55 | local logFile 56 | logFile="${RESTIC_BACKUP_STATS_DIR}/$(date '+%Y')-stats.log.csv" 57 | test -e "$logFile" || install -D -m 0644 <(echo "Date, Snapshot ID, Added, Removed, Snapshot size") "$logFile" 58 | # DEV-NOTE: using `ex` due `sed` inconsistencies (GNU vs. BSD) and `awk` cannot edit in-place. `ex` does a good job 59 | printf '1a\n%s\n.\nwq\n' "$(date '+%F %H:%M:%S'), ${snapId}, ${added}, ${removed}, ${snapSize}" | ex "$logFile" 60 | } 61 | 62 | # Notify the backup summary stats to the user 63 | notifyBackupStats() { 64 | local statsMsg="$1" 65 | if [ -w "$RESTIC_BACKUP_NOTIFICATION_FILE" ]; then 66 | echo "$statsMsg" >> "$RESTIC_BACKUP_NOTIFICATION_FILE" 67 | else 68 | echo "[WARN] Couldn't write to the backup notification file. File not found or not writable: ${RESTIC_BACKUP_NOTIFICATION_FILE}" 69 | fi 70 | } 71 | 72 | # ------------ 73 | # === Main === 74 | # ------------ 75 | 76 | assert_envvars \ 77 | RESTIC_BACKUP_PATHS RESTIC_BACKUP_TAG \ 78 | RESTIC_BACKUP_EXCLUDE_FILE RESTIC_BACKUP_EXTRA_ARGS RESTIC_REPOSITORY RESTIC_VERBOSITY_LEVEL \ 79 | RESTIC_RETENTION_HOURS RESTIC_RETENTION_DAYS RESTIC_RETENTION_MONTHS RESTIC_RETENTION_WEEKS RESTIC_RETENTION_YEARS 80 | 81 | warn_on_missing_envvars \ 82 | B2_ACCOUNT_ID B2_ACCOUNT_KEY B2_CONNECTIONS \ 83 | RESTIC_PASSWORD_FILE 84 | 85 | # Convert to arrays, as arrays should be used to build command lines. See https://github.com/koalaman/shellcheck/wiki/SC2086 86 | IFS=':' read -ra backup_paths <<< "$RESTIC_BACKUP_PATHS" 87 | 88 | # Convert to array, an preserve spaces. See #111 89 | backup_extra_args=( ) 90 | if [ -n "$RESTIC_BACKUP_EXTRA_ARGS" ]; then 91 | while IFS= read -r -d ''; do 92 | backup_extra_args+=( "$REPLY" ) 93 | done < <(xargs printf '%s\0' <<<"$RESTIC_BACKUP_EXTRA_ARGS") 94 | fi 95 | 96 | B2_ARG= 97 | [ -z "${B2_CONNECTIONS+x}" ] || B2_ARG=(--option b2.connections="$B2_CONNECTIONS") 98 | 99 | # If you need to run some commands before performing the backup; create this file, put them there and make the file executable. 100 | PRE_SCRIPT="{{ INSTALL_PREFIX }}/etc/restic/pre_backup.sh" 101 | test -x "$PRE_SCRIPT" && "$PRE_SCRIPT" 102 | 103 | # Set up exclude files: global + path-specific ones 104 | # NOTE that restic will fail the backup if not all listed --exclude-files exist. Thus we should only list them if they are really all available. 105 | ## Global backup configuration. 106 | exclusion_args=(--exclude-file "$RESTIC_BACKUP_EXCLUDE_FILE") 107 | ## Self-contained backup exclusion files per backup path. E.g. having an USB disk at /mnt/media in RESTIC_BACKUP_PATHS, 108 | # then a file /mnt/media/.backup_exclude.txt will automatically be detected and used: 109 | for backup_path in "${backup_paths[@]}"; do 110 | if [ -f "$backup_path/.backup_exclude.txt" ]; then 111 | exclusion_args=("${exclusion_args[@]}" --exclude-file "$backup_path/.backup_exclude.txt") 112 | fi 113 | done 114 | 115 | # --one-file-system is not supportd on Windows (=msys). 116 | FS_ARG= 117 | test "$OSTYPE" = msys || FS_ARG=--one-file-system 118 | 119 | # NOTE start all commands in background and wait for them to finish. 120 | # Reason: bash ignores any signals while child process is executing and thus the trap exit hook is not triggered. 121 | # However if put in subprocesses, wait(1) waits until the process finishes OR signal is received. 122 | # Reference: https://unix.stackexchange.com/questions/146756/forward-sigterm-to-child-in-bash 123 | 124 | # Remove locks from other stale processes to keep the automated backup running. 125 | restic unlock & 126 | wait $! 127 | 128 | # Do the backup! 129 | # See restic-backup(1) or http://restic.readthedocs.io/en/latest/040_backup.html 130 | # --one-file-system makes sure we only backup exactly those mounted file systems specified in $RESTIC_BACKUP_PATHS, and thus not directories like /dev, /sys etc. 131 | # --tag lets us reference these backups later when doing restic-forget. 132 | restic backup \ 133 | --verbose="$RESTIC_VERBOSITY_LEVEL" \ 134 | $FS_ARG \ 135 | --tag "$RESTIC_BACKUP_TAG" \ 136 | "${B2_ARG[@]}" \ 137 | "${exclusion_args[@]}" \ 138 | "${backup_extra_args[@]}" \ 139 | "${backup_paths[@]}" & 140 | wait $! 141 | 142 | # Dereference and delete/prune old backups. 143 | # See restic-forget(1) or http://restic.readthedocs.io/en/latest/060_forget.html 144 | # --group-by only the tag and path, and not by hostname. This is because I create a B2 Bucket per host, and if this hostname accidentially change some time, there would now be multiple backup sets. 145 | restic forget \ 146 | --verbose="$RESTIC_VERBOSITY_LEVEL" \ 147 | --tag "$RESTIC_BACKUP_TAG" \ 148 | "${B2_ARG[@]}" \ 149 | --prune \ 150 | --group-by "paths,tags" \ 151 | --keep-hourly "$RESTIC_RETENTION_HOURS" \ 152 | --keep-daily "$RESTIC_RETENTION_DAYS" \ 153 | --keep-weekly "$RESTIC_RETENTION_WEEKS" \ 154 | --keep-monthly "$RESTIC_RETENTION_MONTHS" \ 155 | --keep-yearly "$RESTIC_RETENTION_YEARS" & 156 | wait $! 157 | 158 | # Check repository for errors. 159 | # NOTE this takes much time (and data transfer from remote repo?), do this in a separate systemd.timer which is run less often. 160 | #restic check & 161 | #wait $! 162 | 163 | echo "Backup & cleaning is done." 164 | 165 | # (optional) Compute backup summary stats 166 | if [[ -n "$RESTIC_BACKUP_STATS_DIR" || -n "$RESTIC_BACKUP_NOTIFICATION_FILE" ]]; then 167 | echo 'Silently computing backup summary stats...' 168 | latest_snapshots=$(restic snapshots --tag "$RESTIC_BACKUP_TAG" --latest 2 --compact \ 169 | | grep -Ei "^[abcdef0-9]{8} " \ 170 | | awk '{print $1}' \ 171 | | tail -2 \ 172 | | tr '\n' ' ') 173 | latest_snapshot_diff=$(echo "$latest_snapshots" | xargs restic diff) 174 | added=$(echo "$latest_snapshot_diff" | grep -i 'added:' | awk '{print $2 " " $3}') 175 | removed=$(echo "$latest_snapshot_diff" | grep -i 'removed:' | awk '{print $2 " " $3}') 176 | snapshot_size=$(restic stats latest --tag "$RESTIC_BACKUP_TAG" | grep -i 'total size:' | cut -d ':' -f2 | xargs) # xargs acts as trim 177 | snapshotId=$(echo "$latest_snapshots" | cut -d ' ' -f2) 178 | statsMsg="Added: ${added}. Removed: ${removed}. Snap size: ${snapshot_size}" 179 | 180 | echo "$statsMsg" 181 | test -n "$RESTIC_BACKUP_STATS_DIR" && logBackupStatsCsv "$snapshotId" "$added" "$removed" "$snapshot_size" 182 | test -n "$RESTIC_BACKUP_NOTIFICATION_FILE" && notifyBackupStats "$statsMsg" 183 | fi 184 | -------------------------------------------------------------------------------- /bin/restic_check.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | # Check the backups made with restic to Backblaze B2 for errors. 3 | # See restic_backup.sh on how this script is run (as it's analogous for this script). 4 | 5 | set -o errexit 6 | set -o pipefail 7 | [[ "${TRACE-0}" =~ ^1|t|y|true|yes$ ]] && set -o xtrace 8 | 9 | # Clean up lock if we are killed. 10 | # If killed by systemd, like $(systemctl stop restic), then it kills the whole cgroup and all it's subprocesses. 11 | # However if we kill this script ourselves, we need this trap that kills all subprocesses manually. 12 | exit_hook() { 13 | echo "In exit_hook(), being killed" >&2 14 | jobs -p | xargs kill 15 | restic unlock 16 | } 17 | trap exit_hook INT TERM 18 | 19 | # Assert that all needed environment variables are set. 20 | assert_envvars() { 21 | local varnames=("$@") 22 | for varname in "${varnames[@]}"; do 23 | if [ -z ${!varname+x} ]; then 24 | printf "%s must be set for this script to work.\n\nDid you forget to source a {{ INSTALL_PREFIX }}/etc/restic/*.env.sh profile in the current shell before executing this script?\n" "$varname" >&2 25 | exit 1 26 | fi 27 | done 28 | } 29 | 30 | warn_on_missing_envvars() { 31 | local unset_envs=() 32 | local varnames=("$@") 33 | for varname in "${varnames[@]}"; do 34 | if [ -z "${!varname}" ]; then 35 | unset_envs=("${unset_envs[@]}" "$varname") 36 | fi 37 | done 38 | 39 | if [ ${#unset_envs[@]} -gt 0 ]; then 40 | printf "The following env variables are recommended, but have not been set. This script may not work as expected: %s\n" "${unset_envs[*]}" >&2 41 | fi 42 | } 43 | 44 | assert_envvars\ 45 | RESTIC_PASSWORD_FILE RESTIC_REPOSITORY RESTIC_VERBOSITY_LEVEL 46 | 47 | warn_on_missing_envvars \ 48 | B2_ACCOUNT_ID B2_ACCOUNT_KEY B2_CONNECTIONS 49 | 50 | B2_ARG= 51 | [ -z "${B2_CONNECTIONS+x}" ] || B2_ARG=(--option b2.connections="$B2_CONNECTIONS") 52 | 53 | # Remove locks from other stale processes to keep the automated backup running. 54 | # NOTE nope, don't unlock like restic_backup.sh. restic_backup.sh should take precedence over this script. 55 | #restic unlock & 56 | #wait $! 57 | 58 | # Check repository for errors. 59 | restic check \ 60 | "${B2_ARG[@]}" \ 61 | --verbose="$RESTIC_VERBOSITY_LEVEL" & 62 | wait $! 63 | -------------------------------------------------------------------------------- /bin/resticw: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | # @generated 3 | 4 | DOC="A little wrapper over restic just to handle profiles and environment loading, with small extensions. 5 | 6 | Usage: 7 | resticw [--profile ] ... 8 | resticw [--profile ] --diff-latest 9 | 10 | The is just the regular unwrapped restic command arguments, e.g. \`stats latest\`. 11 | 12 | Options: 13 | -p --profile= Specify the profile to load or use default [default: default]. 14 | --diff-latest Show latest snapshot changes: Runs \`restic diff\` after finding the latest 2 snapshots. 15 | 16 | Examples: 17 | resticw --profile profileA snapshots 18 | resticw stats latest # this will use the profile: default 19 | resticw -p profileB --diff-latest 20 | 21 | 💡 You may need to run it with sudo to source the profile environment. 22 | " 23 | 24 | # The following argument parser is generated with docopt.sh from the above docstring. 25 | # See https://github.com/andsens/docopt.sh. If the DOC is updated or new options are added, refresh the parser! 26 | 27 | # docopt parser below, refresh this parser with `docopt.sh resticw` 28 | # shellcheck disable=2016,1075,2154 29 | docopt() { parse() { if ${DOCOPT_DOC_CHECK:-true}; then local doc_hash 30 | if doc_hash=$(printf "%s" "$DOC" | (sha256sum 2>/dev/null || shasum -a 256)); then 31 | if [[ ${doc_hash:0:5} != "$digest" ]]; then 32 | stderr "The current usage doc (${doc_hash:0:5}) does not match \ 33 | what the parser was generated with (${digest}) 34 | Run \`docopt.sh\` to refresh the parser."; _return 70; fi; fi; fi 35 | local root_idx=$1; shift; argv=("$@"); parsed_params=(); parsed_values=() 36 | left=(); testdepth=0; local arg; while [[ ${#argv[@]} -gt 0 ]]; do 37 | if [[ ${argv[0]} = "--" ]]; then for arg in "${argv[@]}"; do 38 | parsed_params+=('a'); parsed_values+=("$arg"); done; break 39 | elif [[ ${argv[0]} = --* ]]; then parse_long 40 | elif [[ ${argv[0]} = -* && ${argv[0]} != "-" ]]; then parse_shorts 41 | elif ${DOCOPT_OPTIONS_FIRST:-false}; then for arg in "${argv[@]}"; do 42 | parsed_params+=('a'); parsed_values+=("$arg"); done; break; else 43 | parsed_params+=('a'); parsed_values+=("${argv[0]}"); argv=("${argv[@]:1}"); fi 44 | done; local idx; if ${DOCOPT_ADD_HELP:-true}; then 45 | for idx in "${parsed_params[@]}"; do [[ $idx = 'a' ]] && continue 46 | if [[ ${shorts[$idx]} = "-h" || ${longs[$idx]} = "--help" ]]; then 47 | stdout "$trimmed_doc"; _return 0; fi; done; fi 48 | if [[ ${DOCOPT_PROGRAM_VERSION:-false} != 'false' ]]; then 49 | for idx in "${parsed_params[@]}"; do [[ $idx = 'a' ]] && continue 50 | if [[ ${longs[$idx]} = "--version" ]]; then stdout "$DOCOPT_PROGRAM_VERSION" 51 | _return 0; fi; done; fi; local i=0; while [[ $i -lt ${#parsed_params[@]} ]]; do 52 | left+=("$i"); ((i++)) || true; done 53 | if ! required "$root_idx" || [ ${#left[@]} -gt 0 ]; then error; fi; return 0; } 54 | parse_shorts() { local token=${argv[0]}; local value; argv=("${argv[@]:1}") 55 | [[ $token = -* && $token != --* ]] || _return 88; local remaining=${token#-} 56 | while [[ -n $remaining ]]; do local short="-${remaining:0:1}" 57 | remaining="${remaining:1}"; local i=0; local similar=(); local match=false 58 | for o in "${shorts[@]}"; do if [[ $o = "$short" ]]; then similar+=("$short") 59 | [[ $match = false ]] && match=$i; fi; ((i++)) || true; done 60 | if [[ ${#similar[@]} -gt 1 ]]; then 61 | error "${short} is specified ambiguously ${#similar[@]} times" 62 | elif [[ ${#similar[@]} -lt 1 ]]; then match=${#shorts[@]}; value=true 63 | shorts+=("$short"); longs+=(''); argcounts+=(0); else value=false 64 | if [[ ${argcounts[$match]} -ne 0 ]]; then if [[ $remaining = '' ]]; then 65 | if [[ ${#argv[@]} -eq 0 || ${argv[0]} = '--' ]]; then 66 | error "${short} requires argument"; fi; value=${argv[0]}; argv=("${argv[@]:1}") 67 | else value=$remaining; remaining=''; fi; fi; if [[ $value = false ]]; then 68 | value=true; fi; fi; parsed_params+=("$match"); parsed_values+=("$value"); done 69 | }; parse_long() { local token=${argv[0]}; local long=${token%%=*} 70 | local value=${token#*=}; local argcount; argv=("${argv[@]:1}") 71 | [[ $token = --* ]] || _return 88; if [[ $token = *=* ]]; then eq='='; else eq='' 72 | value=false; fi; local i=0; local similar=(); local match=false 73 | for o in "${longs[@]}"; do if [[ $o = "$long" ]]; then similar+=("$long") 74 | [[ $match = false ]] && match=$i; fi; ((i++)) || true; done 75 | if [[ $match = false ]]; then i=0; for o in "${longs[@]}"; do 76 | if [[ $o = $long* ]]; then similar+=("$long"); [[ $match = false ]] && match=$i 77 | fi; ((i++)) || true; done; fi; if [[ ${#similar[@]} -gt 1 ]]; then 78 | error "${long} is not a unique prefix: ${similar[*]}?" 79 | elif [[ ${#similar[@]} -lt 1 ]]; then 80 | [[ $eq = '=' ]] && argcount=1 || argcount=0; match=${#shorts[@]} 81 | [[ $argcount -eq 0 ]] && value=true; shorts+=(''); longs+=("$long") 82 | argcounts+=("$argcount"); else if [[ ${argcounts[$match]} -eq 0 ]]; then 83 | if [[ $value != false ]]; then 84 | error "${longs[$match]} must not have an argument"; fi 85 | elif [[ $value = false ]]; then 86 | if [[ ${#argv[@]} -eq 0 || ${argv[0]} = '--' ]]; then 87 | error "${long} requires argument"; fi; value=${argv[0]}; argv=("${argv[@]:1}") 88 | fi; if [[ $value = false ]]; then value=true; fi; fi; parsed_params+=("$match") 89 | parsed_values+=("$value"); }; required() { local initial_left=("${left[@]}") 90 | local node_idx; ((testdepth++)) || true; for node_idx in "$@"; do 91 | if ! "node_$node_idx"; then left=("${initial_left[@]}"); ((testdepth--)) || true 92 | return 1; fi; done; if [[ $((--testdepth)) -eq 0 ]]; then 93 | left=("${initial_left[@]}"); for node_idx in "$@"; do "node_$node_idx"; done; fi 94 | return 0; }; either() { local initial_left=("${left[@]}"); local best_match_idx 95 | local match_count; local node_idx; ((testdepth++)) || true 96 | for node_idx in "$@"; do if "node_$node_idx"; then 97 | if [[ -z $match_count || ${#left[@]} -lt $match_count ]]; then 98 | best_match_idx=$node_idx; match_count=${#left[@]}; fi; fi 99 | left=("${initial_left[@]}"); done; ((testdepth--)) || true 100 | if [[ -n $best_match_idx ]]; then "node_$best_match_idx"; return 0; fi 101 | left=("${initial_left[@]}"); return 1; }; optional() { local node_idx 102 | for node_idx in "$@"; do "node_$node_idx"; done; return 0; }; oneormore() { 103 | local i=0; local prev=${#left[@]}; while "node_$1"; do ((i++)) || true 104 | [[ $prev -eq ${#left[@]} ]] && break; prev=${#left[@]}; done 105 | if [[ $i -ge 1 ]]; then return 0; fi; return 1; }; switch() { local i 106 | for i in "${!left[@]}"; do local l=${left[$i]} 107 | if [[ ${parsed_params[$l]} = "$2" ]]; then 108 | left=("${left[@]:0:$i}" "${left[@]:((i+1))}") 109 | [[ $testdepth -gt 0 ]] && return 0; if [[ $3 = true ]]; then 110 | eval "((var_$1++))" || true; else eval "var_$1=true"; fi; return 0; fi; done 111 | return 1; }; value() { local i; for i in "${!left[@]}"; do local l=${left[$i]} 112 | if [[ ${parsed_params[$l]} = "$2" ]]; then 113 | left=("${left[@]:0:$i}" "${left[@]:((i+1))}") 114 | [[ $testdepth -gt 0 ]] && return 0; local value 115 | value=$(printf -- "%q" "${parsed_values[$l]}"); if [[ $3 = true ]]; then 116 | eval "var_$1+=($value)"; else eval "var_$1=$value"; fi; return 0; fi; done 117 | return 1; }; stdout() { printf -- "cat <<'EOM'\n%s\nEOM\n" "$1"; }; stderr() { 118 | printf -- "cat <<'EOM' >&2\n%s\nEOM\n" "$1"; }; error() { 119 | [[ -n $1 ]] && stderr "$1"; stderr "$usage"; _return 1; }; _return() { 120 | printf -- "exit %d\n" "$1"; exit "$1"; }; set -e; trimmed_doc=${DOC:0:751} 121 | usage=${DOC:102:105}; digest=a9466; shorts=(-p '') 122 | longs=(--profile --diff-latest); argcounts=(1 0); node_0(){ value __profile 0; } 123 | node_1(){ switch __diff_latest 1; }; node_2(){ 124 | value _restic_arguments_line_ a true; }; node_3(){ optional 0; }; node_4(){ 125 | oneormore 2; }; node_5(){ required 3 4; }; node_6(){ required 3 1; }; node_7(){ 126 | either 5 6; }; node_8(){ required 7; }; cat <<<' docopt_exit() { 127 | [[ -n $1 ]] && printf "%s\n" "$1" >&2; printf "%s\n" "${DOC:102:105}" >&2 128 | exit 1; }'; unset var___profile var___diff_latest var__restic_arguments_line_ 129 | parse 8 "$@"; local prefix=${DOCOPT_PREFIX:-''}; unset "${prefix}__profile" \ 130 | "${prefix}__diff_latest" "${prefix}_restic_arguments_line_" 131 | eval "${prefix}"'__profile=${var___profile:-default}' 132 | eval "${prefix}"'__diff_latest=${var___diff_latest:-false}' 133 | if declare -p var__restic_arguments_line_ >/dev/null 2>&1; then 134 | eval "${prefix}"'_restic_arguments_line_=("${var__restic_arguments_line_[@]}")' 135 | else eval "${prefix}"'_restic_arguments_line_=()'; fi; local docopt_i=1 136 | [[ $BASH_VERSION =~ ^4.3 ]] && docopt_i=2; for ((;docopt_i>0;docopt_i--)); do 137 | declare -p "${prefix}__profile" "${prefix}__diff_latest" \ 138 | "${prefix}_restic_arguments_line_"; done; } 139 | # docopt parser above, complete command for generating this parser is `docopt.sh resticw` 140 | 141 | # Parse arguments - See https://github.com/andsens/docopt.sh for the magic :) 142 | DOCOPT_OPTIONS_FIRST=true # treat everything after the first non-option as commands/arguments 143 | eval "$(docopt "$@")" 144 | 145 | # --^^^-- END OF GENERATED COMMAND LINE PARSING STUFF --^^^-- 146 | # 147 | # --vvv-- ACTUAL SCRIPT BELOW --vvv-- 148 | 149 | # Exit on error, unbound variable, pipe error 150 | set -euo pipefail 151 | ENV_DIR="{{ INSTALL_PREFIX }}/etc/restic" 152 | 153 | ERR_NO_SUCH_PROFILE=2 154 | ERR_PROFILE_NO_READ_PERM=3 155 | 156 | # Compute the latest 2 snapshots and run the diff 157 | latestSnapshotDiff() { 158 | restic snapshots --tag "$RESTIC_BACKUP_TAG" --latest 2 --compact \ 159 | | grep -Ei "^[abcdef0-9]{8} " \ 160 | | awk '{print $1}' \ 161 | | tail -2 \ 162 | | tr '\n' ' ' \ 163 | | xargs restic diff 164 | } 165 | 166 | # shellcheck disable=SC2154 167 | profile_file="${ENV_DIR}/${__profile}.env.sh" 168 | 169 | [[ ! -f "$profile_file" ]] && echo "Invalid profile: No such environment file ${profile_file}" && exit "$ERR_NO_SUCH_PROFILE" 170 | 171 | if [[ ! -r "$profile_file" ]]; then 172 | echo "Error: Could not read the environment file ${profile_file}. Are you running this script as the correct user? Maybe try sudo with the right user." 173 | exit "$ERR_PROFILE_NO_READ_PERM" 174 | fi 175 | 176 | echo -e "‣ Using profile: ${__profile} -- (${profile_file})\n" 177 | # shellcheck disable=SC1090 178 | source "$profile_file" 179 | 180 | # shellcheck disable=SC2154 181 | if [[ "${__diff_latest}" == true ]]; then 182 | latestSnapshotDiff 183 | else 184 | restic "${_restic_arguments_line_[@]}" 185 | fi 186 | -------------------------------------------------------------------------------- /bin/systemd-email: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | # Send email notification from systemd. 3 | # Source: https://serverfault.com/questions/876233/how-to-send-an-email-if-a-systemd-service-is-restarted 4 | # Source: https://wiki.archlinux.org/index.php/Systemd/Timers#MAILTO 5 | # Usage: systemd-email 6 | 7 | 8 | # According to 9 | # http://www.flashissue.com/blog/gmail-sending-limits/ 10 | # Gmail blocks your account if you send more than 500 emails per day, which is one email every 11 | # (24 * 60 * 60) / 500 = 172.8 second => choose a min wait time which is significantly longer than this to be on the safe time to not exceed 500 emails per day. 12 | # However this source 13 | # https://group-mail.com/sending-email/email-send-limits-and-options/ 14 | # says the limit when not using the Gmail webinterface but going directly to the SMTP server is 100-150 per day, which yelds maximum one email every 15 | # (24 * 60 * 60) / 100 = 864 second 16 | # One option that I used with my old Axis cameras it to use my gmx.com accunt for sending emails instead, as there are (no?) higher limits there. 17 | MIN_WAIT_TIME_S=900 18 | SCRIPT_NAME=$(basename "$0") 19 | LAST_RUN_FILE="/tmp/${SCRIPT_NAME}_last_run.txt" 20 | 21 | last_touch() { 22 | stat -c %Y "$1" 23 | } 24 | 25 | waited_long_enough() { 26 | retval=1 27 | if [ -e "$LAST_RUN_FILE" ]; then 28 | now=$(date +%s) 29 | last=$(last_touch "$LAST_RUN_FILE") 30 | wait_s=$((now - last)) 31 | if [ "$wait_s" -gt "$MIN_WAIT_TIME_S" ]; then 32 | retval=0 33 | fi 34 | else 35 | retval=0 36 | fi 37 | 38 | [ $retval -eq 0 ] && touch "$LAST_RUN_FILE" 39 | return $retval 40 | } 41 | 42 | 43 | # Make sure that my Gmail account dont' get shut down because of sending too many emails! 44 | if ! waited_long_enough; then 45 | echo "Systemd email was not sent, as it's less than ${MIN_WAIT_TIME_S} seconds since the last one was sent." 46 | exit 1 47 | fi 48 | 49 | 50 | recipient=$1 51 | system_unit=$2 52 | 53 | sendmail -t < 56 | Subject: [systemd-email] ${system_unit} 57 | Content-Transfer-Encoding: 8bit 58 | Content-Type: text/plain; charset=UTF-8 59 | 60 | $(systemctl status --full "$system_unit") 61 | ERRMAIL 62 | -------------------------------------------------------------------------------- /etc/cron.d/restic: -------------------------------------------------------------------------------- 1 | SHELL=/bin/sh 2 | PATH=/etc:/bin:/sbin:/usr/bin:/usr/sbin:/usr/local/bin/:{{ INSTALL_PREFIX }}/bin/ 3 | # Order of crontab fields 4 | # minute hour mday month wday command 5 | # Reference: https://www.freebsd.org/doc/handbook/configtuning-cron.html 6 | # Reference: crontab(5). 7 | 8 | @midnight root . {{ INSTALL_PREFIX }}/etc/restic/default.env.sh && restic_backup.sh 9 | @monthly root . {{ INSTALL_PREFIX }}/etc/restic/default.env.sh && restic_check.sh 10 | 11 | # Email notification version. Make sure bin/cron_mail is in the above $PATH 12 | #@midnight root . {{ INSTALL_PREFIX }}/etc/restic/default.env.sh && cron_mail restic_backup.sh 13 | #@monthly root . {{ INSTALL_PREFIX }}/etc/restic/default.env.sh && cron_mail restic_check.sh 14 | -------------------------------------------------------------------------------- /etc/restic/_global.env.sh: -------------------------------------------------------------------------------- 1 | # shellcheck shell=sh 2 | 3 | # Global environment variables 4 | # These variables are sourced FIRST, and any values inside of *.env.sh files for 5 | # specific configurations will override if also defined there. 6 | 7 | 8 | # Official instructions on how to setup the restic variables for Backblaze B2 can be found at 9 | # https://restic.readthedocs.io/en/latest/030_preparing_a_new_repo.html#backblaze-b2 10 | 11 | 12 | # The restic repository encryption key 13 | export RESTIC_PASSWORD_FILE="{{ INSTALL_PREFIX }}/etc/restic/pw.txt" 14 | # The global restic exclude file 15 | export RESTIC_BACKUP_EXCLUDE_FILE="{{ INSTALL_PREFIX }}/etc/restic/backup_exclude.txt" 16 | 17 | # Backblaze B2 credentials keyID & applicationKey pair. 18 | # Restic environment variables are documented at https://restic.readthedocs.io/en/latest/040_backup.html#environment-variables 19 | export B2_ACCOUNT_ID="" # *EDIT* fill with your keyID 20 | export B2_ACCOUNT_KEY="" # *EDIT* fill with your applicationKey 21 | 22 | # How many network connections to set up to B2. Default is 5. 23 | export B2_CONNECTIONS=10 24 | 25 | # Optional extra space-separated args to restic-backup. 26 | # This is empty here and profiles can override this after sourcing this file. 27 | export RESTIC_BACKUP_EXTRA_ARGS= 28 | 29 | # Verbosity level from 0-3. 0 means no --verbose. 30 | # Override this value in a profile if needed. 31 | export RESTIC_VERBOSITY_LEVEL=0 32 | 33 | # (optional, uncomment to enable) Backup summary stats log: snapshot size, etc. (empty/unset won't log) 34 | #export RESTIC_BACKUP_STATS_DIR="{{ INSTALL_PREFIX }}/var/log/restic-automatic-backup-scheduler" 35 | 36 | # (optional) Desktop notifications. See README and restic_backup.sh for details on how to set this up (empty/unset means disabled) 37 | export RESTIC_BACKUP_NOTIFICATION_FILE= 38 | -------------------------------------------------------------------------------- /etc/restic/backup_exclude.txt: -------------------------------------------------------------------------------- 1 | /.snapshots/ 2 | /opt 3 | /root/.cache/ 4 | /usr/share/**/*.html 5 | /usr/share/help/ 6 | /usr/share/licenses/ 7 | /usr/share/man/ 8 | /usr/src/ 9 | /var/cache/ 10 | /var/log/ 11 | -------------------------------------------------------------------------------- /etc/restic/default.env.sh: -------------------------------------------------------------------------------- 1 | # shellcheck shell=sh 2 | 3 | # This is the default profile. Fill it with your desired configuration. 4 | # Additionally, you can create and use more profiles by copying this file. 5 | 6 | # This file (and other .env.sh files) has two purposes: 7 | # - being sourced by systemd timers to setup the backup before running restic_backup.sh 8 | # - being sourced in a user's shell to work directly with restic commands e.g. 9 | # $ source /etc/restic/default.env.sh 10 | # $ restic snapshots 11 | # Thus you don't have to provide all the arguments like 12 | # $ restic --repo ... --password-file ... 13 | 14 | # shellcheck source=etc/restic/_global.env.sh 15 | . "{{ INSTALL_PREFIX }}/etc/restic/_global.env.sh" 16 | 17 | # Envvars below will override those in _global.env.sh if present. 18 | 19 | export RESTIC_REPOSITORY="b2:" # *EDIT* fill with your repo name 20 | 21 | # What to backup. Colon-separated paths e.g. to different mountpoints "/home:/mnt/usb_disk". 22 | # To backup only your home directory, set "/home/your-user" 23 | export RESTIC_BACKUP_PATHS="" # *EDIT* fill conveniently with one or multiple paths 24 | 25 | 26 | # Example below of how to dynamically add a path that is mounted e.g. external USB disk. 27 | # restic does not fail if a specified path is not mounted, but it's nicer to only add if they are available. 28 | #test -d /mnt/media && RESTIC_BACKUP_PATHS+=" /mnt/media" 29 | 30 | # A tag to identify backup snapshots. 31 | export RESTIC_BACKUP_TAG=systemd.timer 32 | 33 | # Retention policy - How many backups to keep. 34 | # See https://restic.readthedocs.io/en/stable/060_forget.html?highlight=month#removing-snapshots-according-to-a-policy 35 | export RESTIC_RETENTION_HOURS=1 36 | export RESTIC_RETENTION_DAYS=14 37 | export RESTIC_RETENTION_WEEKS=16 38 | export RESTIC_RETENTION_MONTHS=18 39 | export RESTIC_RETENTION_YEARS=3 40 | 41 | # Optional extra space-separated arguments to restic-backup. 42 | # Example: Add two additional exclude files to the global one in RESTIC_PASSWORD_FILE. 43 | #RESTIC_BACKUP_EXTRA_ARGS="--exclude-file /path/to/extra/exclude/file/a --exclude-file /path/to/extra/exclude/file/b" 44 | # Example: exclude all directories that have a .git/ directory inside it. 45 | #RESTIC_BACKUP_EXTRA_ARGS="--exclude-if-present .git" 46 | -------------------------------------------------------------------------------- /etc/restic/pw.txt: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /img/macos_notification.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/erikw/restic-automatic-backup-scheduler/4158ee155ae7e84ecc14faa60cbc9e3ec9e2c3d6/img/macos_notification.png -------------------------------------------------------------------------------- /img/pen-paper.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/erikw/restic-automatic-backup-scheduler/4158ee155ae7e84ecc14faa60cbc9e3ec9e2c3d6/img/pen-paper.png -------------------------------------------------------------------------------- /img/plus.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/erikw/restic-automatic-backup-scheduler/4158ee155ae7e84ecc14faa60cbc9e3ec9e2c3d6/img/plus.png -------------------------------------------------------------------------------- /img/readme_sections.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/erikw/restic-automatic-backup-scheduler/4158ee155ae7e84ecc14faa60cbc9e3ec9e2c3d6/img/readme_sections.png -------------------------------------------------------------------------------- /img/tasksched.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/erikw/restic-automatic-backup-scheduler/4158ee155ae7e84ecc14faa60cbc9e3ec9e2c3d6/img/tasksched.png -------------------------------------------------------------------------------- /scripts/devcontainer_postCreateCommand.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | # Devcontainer postCreateCommand. 3 | # Install dependencies for running this project in GitHub Codespaces. 4 | 5 | set -eux 6 | 7 | # For git version tagging: 8 | go install github.com/maykonlsf/semver-cli/cmd/semver@latest 9 | -------------------------------------------------------------------------------- /usr/lib/systemd/system/nm-unmetered-connection.service: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Description=Check if the current NetworkManager connection is metered 3 | 4 | [Service] 5 | Type=oneshot 6 | ExecStart={{ INSTALL_PREFIX }}/bin/nm-unmetered-connection.sh 7 | -------------------------------------------------------------------------------- /usr/lib/systemd/system/restic-backup@.service: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Description=Backup with restic to Backblaze B2 3 | # Email on failure require special setup. See README.md 4 | #OnFailure=status-email-user@%n.service 5 | # Prevent backup on unmetered connection. Needs special setup. See README.md. 6 | #Requires=nm-unmetered-connection.service 7 | #After=nm-unmetered-connection.service 8 | 9 | [Service] 10 | Type=simple 11 | Nice=10 12 | # $HOME or $XDG_CACHE_HOME must be set for restic to find /root/.cache/restic/ 13 | Environment="HOME=/root" 14 | # pipefail: so that redirecting stderr from the script to systemd-cat does not hide the failed command from OnFailure above. 15 | # Random sleep (in seconds): in the case of multiple backup profiles. Many restic instances started at the same time could case high load or network bandwith usage. 16 | # `systemd-cat` allows showing the restic output to the systemd journal 17 | ExecStart=/bin/bash -c 'set -o pipefail; ps cax | grep -q restic && sleep $(shuf -i 0-300 -n 1); source {{ INSTALL_PREFIX }}/etc/restic/%I.env.sh && {{ INSTALL_PREFIX }}/bin/restic_backup.sh 2>&1 | systemd-cat' 18 | -------------------------------------------------------------------------------- /usr/lib/systemd/system/restic-backup@.timer: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Description=Backup with restic on schedule 3 | 4 | [Timer] 5 | OnCalendar=daily 6 | Persistent=true 7 | 8 | [Install] 9 | WantedBy=timers.target 10 | -------------------------------------------------------------------------------- /usr/lib/systemd/system/restic-check@.service: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Description=Check restic backup Backblaze B2 for errors 3 | # Email on failure require special setup. See README.md 4 | #OnFailure=status-email-user@%n.service 5 | Conflicts=restic-backup.service 6 | # Prevent backup on unmetered connection. Needs special setup. See README.md. 7 | #Requires=nm-unmetered-connection.service 8 | #After=nm-unmetered-connection.service 9 | 10 | [Service] 11 | Type=simple 12 | Nice=10 13 | # pipefail: so that redirecting stderr from the script to systemd-cat does not hide the failed command from OnFailure above. 14 | # `systemd-cat`: allows showing the restic output to the systemd journal 15 | ExecStart=/bin/bash -c 'set -o pipefail; source {{ INSTALL_PREFIX }}/etc/restic/%I.env.sh && {{ INSTALL_PREFIX }}/bin/restic_check.sh 2>&1 | systemd-cat' 16 | -------------------------------------------------------------------------------- /usr/lib/systemd/system/restic-check@.timer: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Description=Check restic backup Backblaze B2 for errors on a schedule 3 | 4 | [Timer] 5 | OnCalendar=monthly 6 | Persistent=true 7 | 8 | [Install] 9 | WantedBy=timers.target 10 | -------------------------------------------------------------------------------- /usr/lib/systemd/system/status-email-user@.service: -------------------------------------------------------------------------------- 1 | # Source: https://serverfault.com/questions/876233/how-to-send-an-email-if-a-systemd-service-is-restarted 2 | # Source: https://wiki.archlinux.org/index.php/Systemd/Timers#MAILTO 3 | 4 | [Unit] 5 | Description=Send status email for %i to user 6 | 7 | [Service] 8 | Type=oneshot 9 | ExecStart={{ INSTALL_PREFIX }}/bin/systemd-email abc@gmail.com %i 10 | User=root 11 | Group=systemd-journal 12 | --------------------------------------------------------------------------------