├── Config ├── RcloneConfig │ └── .gitkeep ├── DockerConfig │ ├── DefaultDockerConfig.txt │ └── ExampleDockerConfig.txt ├── FilterConfig │ ├── ResticFilter │ │ └── DefaultResticFilter.txt │ └── RcloneFilter │ │ └── DefaultRcloneFilter.txt └── RepositoryPassword │ └── restic-repo.password.txt ├── SetupInstruction ├── 2. Create_Rclone_Config.txt ├── 3. Setup_Job.txt ├── 1. Create_Repository.txt ├── 4. Interact_Restic.txt └── 5. Restore_Data.txt ├── LICENSE ├── Executor ├── ResticPruneExec ├── ResticForgetExec ├── RcloneExec ├── ResticBackupExec ├── Notifier ├── ConditionHandler ├── MainExec └── DockerHandler ├── Jobs └── JobTemplate └── README.md /Config/RcloneConfig/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /Config/DockerConfig/DefaultDockerConfig.txt: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /Config/FilterConfig/ResticFilter/DefaultResticFilter.txt: -------------------------------------------------------------------------------- 1 | # Docs: "https://restic.readthedocs.io/en/latest/040_backup.html#excluding-files" 2 | ########################## Excludes ########################## 3 | 4 | # Exclusion of logging files 5 | *.log 6 | -------------------------------------------------------------------------------- /Config/RepositoryPassword/restic-repo.password.txt: -------------------------------------------------------------------------------- 1 | Replace this text with your restic repository password in clear text. 2 | Save the file and remove the .txt extension from the filename. 3 | If you want to change the filename, you need to change the name in 4 | the restic docker commands and also in your repository. 5 | -------------------------------------------------------------------------------- /Config/FilterConfig/RcloneFilter/DefaultRcloneFilter.txt: -------------------------------------------------------------------------------- 1 | # Docs: https://rclone.org/filtering/#patterns-for-matching-path-file-names 2 | ########################## includes ########################## 3 | #+ include pattern 4 | 5 | ########################## excludes ########################## 6 | #- exclude pattern 7 | 8 | # Exclude complete directory 9 | #- Directory/** 10 | 11 | # Exclusion of logging files 12 | - *.log 13 | 14 | # Exclusion of logging files 15 | - *.log 16 | -------------------------------------------------------------------------------- /SetupInstruction/2. Create_Rclone_Config.txt: -------------------------------------------------------------------------------- 1 | ###################### Create Rclone Config ##################### 2 | 3 | # Run this docker command to create or change your rclone configurations. 4 | # Replace /PATH/TO/ to the location of your BackupScript directory. 5 | # Rclone will start an interactive session where you will be guided 6 | # through the configuration process. 7 | # Visit the rclone documentations to setup your config. 8 | 9 | # https://rclone.org/docs/ 10 | 11 | 12 | 13 | docker run -i --rm \ 14 | --volume /PATH/TO/BackupScript/Config/RcloneConfig:/config/rclone \ 15 | --user $(id -u):$(id -g) \ 16 | rclone/rclone config -------------------------------------------------------------------------------- /SetupInstruction/3. Setup_Job.txt: -------------------------------------------------------------------------------- 1 | ###################### Make Job Executable ################ 2 | 3 | # Use to make a job file executable 4 | # Change the /PATH/TO/ to point to BackupScript directory 5 | # Change YourJobFileName to the name of your job file 6 | 7 | 8 | chmod +x /PATH/TO/BackupScript/Jobs/YourJobFileName 9 | 10 | 11 | ###################### Create Cron Job ################ 12 | # https://www.freecodecamp.org/news/cron-jobs-in-linux/ 13 | 14 | # Open a terminal window on your system. 15 | # Open your cron job overview with an editor using 16 | 17 | contab -e 18 | 19 | # Add a job to the file. Example: 20 | 21 | * 1 * * * /PATH/TO/BackupScript/Jobs/YourJobFileName 22 | -------------------------------------------------------------------------------- /SetupInstruction/1. Create_Repository.txt: -------------------------------------------------------------------------------- 1 | ###################### Create New Repository ################ 2 | 3 | # Run this command to create a new restic repository 4 | # Replace /PATH/TO/ with the location of your BackupScript directory 5 | # Create a new empty directory for your repository in the desired location 6 | # Replace /PATH/TO/YOUR/REPOSITORIE with the path to your repository 7 | # Edit or create a new restic-repo.password file under "BackupScript > Config > RepositoryPassword" 8 | # Replace PASSWORD.FILE with the correct file name 9 | # If you want to have different passwords you can change the name of the file. 10 | # Do not change the location of the password files 11 | # Please make sure to change the name in below docker command and also your backup job 12 | # Run the command 13 | 14 | # https://restic.readthedocs.io/en/latest/ 15 | 16 | 17 | 18 | docker run -i --rm \ 19 | --volume /PATH/TO/BackupScript:/home \ 20 | --volume /PATH/TO/YOUR/REPOSITORIE:/repo \ 21 | --user $(id -u):$(id -g) \ 22 | restic/restic \ 23 | --password-file=/home/Config/RepositoryPassword/PASSWORD.FILE \ 24 | init -r repo -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 David Krumm 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Config/DockerConfig/ExampleDockerConfig.txt: -------------------------------------------------------------------------------- 1 | # Configuration for Docker containers that are stopped during the backup and started again afterwards. 2 | # 3 | # Format: 4 | # Add Container to start/stop 5 | # + ContainerName_or_ID 6 | # 7 | # Exclude Container from start/stop 8 | # - ContainerName_or_ID 9 | # 10 | # Behavior: 11 | # The list is processed from top to bottom. 12 | # The first entry in the list is stopped/started first. 13 | # When "reverse_docker_start_sequence" is set to "true", the list is processed in reverse order for starting. 14 | # Comments and empty lines are ignored. 15 | # If no containers are to be stopped, the file can be left empty. 16 | 17 | 18 | # Examples: 19 | 20 | # Add individual containers to proceed in a specific order 21 | + FirstContainerToStop 22 | + SecondContainerToStop 23 | 24 | # Use pattern: 25 | # Adds "Container" "Container-Server" "Container-Database" 26 | + Contai* 27 | 28 | # Adds "Container1-Server" "Container2-Server"; Dose not add "Container-Database" 29 | + Contai*-Server 30 | 31 | 32 | # To stop all containers or all remaining ones. 33 | + * 34 | 35 | 36 | # Excluded explicitly from starting and stopping. 37 | - ContainerName 38 | - Contai* 39 | - Contai*-Server 40 | -------------------------------------------------------------------------------- /SetupInstruction/4. Interact_Restic.txt: -------------------------------------------------------------------------------- 1 | ###################### Interact with Repository ################ 2 | 3 | # Use this Docker command to interact with the repository. 4 | # Replace /PATH/TO/ to the location of your BackupScript directory. 5 | # Replace /PATH/TO/YOUR/REPOSITORIE to the location of your Repository directory. 6 | # Replace PASSWORD.FILE with the correct file name 7 | # Replace YourResticCommands with the commands you want to use. 8 | 9 | # https://restic.readthedocs.io/en/latest/ 10 | 11 | 12 | 13 | docker run -i --rm \ 14 | --volume /PATH/TO/BackupScript:/home \ 15 | --volume /PATH/TO/YOUR/REPOSITORIE:/repo \ 16 | --user $(id -u):$(id -g) \ 17 | restic/restic \ 18 | --password-file=/home/Config/RepositoryPassword/PASSWORD.FILE \ 19 | -r repo \ 20 | YourResticCommands 21 | 22 | 23 | 24 | ###################### Remove Lock from Repository ################ 25 | 26 | # Use this Docker command to remove a lock of your repository. 27 | # Replace /PATH/TO/ to the location of your BackupScript directory. 28 | # Replace /PATH/TO/YOUR/REPOSITORIE to the location of your Repository directory. 29 | 30 | # https://restic.readthedocs.io/en/latest/ 31 | 32 | 33 | 34 | docker run -i --rm \ 35 | --volume /PATH/TO/BackupScript:/home \ 36 | --volume /PATH/TO/YOUR/REPOSITORIE:/repo \ 37 | --user $(id -u):$(id -g) \ 38 | restic/restic \ 39 | --password-file=/home/Config/RepositoryPassword/PASSWORD.FILE \ 40 | -r repo unlock -------------------------------------------------------------------------------- /SetupInstruction/5. Restore_Data.txt: -------------------------------------------------------------------------------- 1 | ###################### Mount Repo for Data Restore ######### 2 | 3 | ###################### Mounting 4 | 5 | # Run this command to mount your repository to a empty folder 6 | # Replace /PATH/TO/ to the location of your BackupScript directory 7 | # Replace /PATH/TO/YOUR/DATA to your data directory 8 | # Replace /PATH/TO/YOUR/REPOSITORIE to your repository 9 | # Replace PASSWORD.FILE with the correct file name 10 | # Replace /PATH/TO/EMPTY/DIR to a empty directory 11 | # Run the command and keep the terminal window open 12 | 13 | # https://restic.readthedocs.io/en/latest/ 14 | 15 | 16 | 17 | docker run --rm \ 18 | --name ResticMounted \ 19 | --device /dev/fuse --cap-add SYS_ADMIN \ 20 | --volume /PATH/TO/BackupScript:/home \ 21 | --volume /PATH/TO/YOUR/DATA:/restore \ 22 | --volume /PATH/TO/YOUR/REPOSITORIE:/repo \ 23 | --volume /PATH/TO/EMPTY/DIR:/mountdir \ 24 | --user $(id -u):$(id -g) \ 25 | restic/restic \ 26 | --password-file=/home/Config/RepositoryPassword/PASSWORD.FILE \ 27 | -r repo mount /mountdir 28 | 29 | 30 | 31 | 32 | ###################### Enter Mount 33 | # Run this command in a second terminal window to enter the mounted restic instance 34 | 35 | docker exec -it ResticMounted sh 36 | 37 | 38 | ###################### Enter Filesystem 39 | # Run this command after you entered the restic instance to enter your repository 40 | 41 | cd mountdir 42 | 43 | 44 | ###################### Copy Files 45 | # Copy file or directories from source path to destination paht including all metadata like rights and ownership 46 | 47 | cp -a /SOURCE-PATH/IN/REPOSITORY /restore/PATH/TO/DESTINATION -------------------------------------------------------------------------------- /Executor/ResticPruneExec: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # BackupScript version 2.0.0 3 | #################################### License ################################ 4 | # MIT License Copyright (c) 2023 David Krumm # 5 | # All rights reserved. # 6 | # # 7 | # This source code is licensed under the MIT license found in the # 8 | # LICENSE file in the root directory of this source tree. # 9 | ############################################################################# 10 | 11 | ######################### Docker Volume Propagation ######################### 12 | 13 | repo="$REPOSITORY" 14 | 15 | if [[ $repo == *":"* ]]; then 16 | repo_prop=":$(echo "$repo" | cut -d ":" -f 2)" 17 | repo=$(echo "$repo" | cut -d ":" -f 1) 18 | else 19 | repo_prop="" 20 | fi 21 | 22 | ################################### Restic ################################## 23 | 24 | $NOTIFIER 25 | $NOTIFIER --message "Starting 'Prune' job" 26 | $NOTIFIER --message "Repository '$repo'" 27 | 28 | ################################### Prune 29 | 30 | cmd="docker run --rm --name $JOB_NAME-ResticPrune \ 31 | --volume $(dirname "$RESTIC_PW"):/password \ 32 | --volume $repo:/repo$repo_prop \ 33 | --user $(id -u):$(id -g) \ 34 | restic/restic \ 35 | --password-file=/password/$(basename "$RESTIC_PW") \ 36 | -r /repo prune" 37 | 38 | # progress message 39 | $NOTIFIER 40 | $NOTIFIER --message "Prune in progress ... " 41 | $NOTIFIER --timestamps "false" 42 | #$NOTIFIER --message "$cmd" --timestamps "false" 43 | 44 | output=$(eval "$cmd" 2>&1) 45 | exit_code=$? 46 | $NOTIFIER --message "$output" --timestamps "false" 47 | 48 | ################################# Evaluation ################################ 49 | 50 | if [[ "$exit_code" == 0 ]]; then 51 | $NOTIFIER --timestamps "false" 52 | $NOTIFIER --message "Completed 'Prune' job of '$repo' successfully" 53 | exit 0 54 | else 55 | $NOTIFIER --timestamps "false" 56 | $NOTIFIER --channel "important" --type "warning" --message "ERROR $JOB_NAME: Prune of '$repo' failed with exit_code=$exit_code" 57 | $NOTIFIER --timestamps "false" 58 | exit 1 59 | fi -------------------------------------------------------------------------------- /Executor/ResticForgetExec: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # BackupScript version 2.0.0 3 | #################################### License ################################ 4 | # MIT License Copyright (c) 2023 David Krumm # 5 | # All rights reserved. # 6 | # # 7 | # This source code is licensed under the MIT license found in the # 8 | # LICENSE file in the root directory of this source tree. # 9 | ############################################################################# 10 | 11 | ################################# Parameters ################################ 12 | 13 | keep_rules="$1" # Rules for keeping snapshots 14 | 15 | ######################### Docker Volume Propagation ######################### 16 | 17 | repo="$REPOSITORY" 18 | 19 | if [[ $repo == *":"* ]]; then 20 | repo_prop=":$(echo "$repo" | cut -d ":" -f 2)" 21 | repo=$(echo "$repo" | cut -d ":" -f 1) 22 | else 23 | repo_prop="" 24 | fi 25 | 26 | ################################### Restic ################################## 27 | 28 | $NOTIFIER 29 | $NOTIFIER --message "Starting 'Forget' job" 30 | $NOTIFIER --message "Repository '$repo'" 31 | 32 | ################################### Forget 33 | 34 | cmd="docker run --rm --name $JOB_NAME-ResticForget \ 35 | --volume $(dirname "$RESTIC_PW"):/password \ 36 | --volume $repo:/repo$repo_prop \ 37 | --user $(id -u):$(id -g) \ 38 | restic/restic \ 39 | --password-file=/password/$(basename "$RESTIC_PW") \ 40 | -r /repo forget $keep_rules" 41 | 42 | # progress message 43 | $NOTIFIER 44 | $NOTIFIER --message "Forget in progress ... " 45 | $NOTIFIER --timestamps "false" 46 | #$NOTIFIER --message "$cmd" --timestamps "false" 47 | 48 | output=$(eval "$cmd" 2>&1) 49 | exit_code=$? 50 | $NOTIFIER --message "$output" --timestamps "false" 51 | 52 | ################################# Evaluation ################################ 53 | 54 | if [[ "$exit_code" == 0 ]]; then 55 | $NOTIFIER --timestamps "false" 56 | $NOTIFIER --message "Completed 'Forget' job of '$repo' successfully" 57 | exit 0 58 | else 59 | $NOTIFIER --timestamps "false" 60 | $NOTIFIER --channel "important" --type "warning" --message "ERROR $JOB_NAME: Forget of '$repo' failed with exit_code=$exit_code" 61 | $NOTIFIER --timestamps "false" 62 | exit 1 63 | fi -------------------------------------------------------------------------------- /Executor/RcloneExec: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # BackupScript version 2.0.0 3 | #################################### License ################################ 4 | # MIT License Copyright (c) 2023 David Krumm # 5 | # All rights reserved. # 6 | # # 7 | # This source code is licensed under the MIT license found in the # 8 | # LICENSE file in the root directory of this source tree. # 9 | ############################################################################# 10 | 11 | ################################# Parameters ################################ 12 | 13 | logging_options="$1" # Settings for the logging of rclone 14 | options="$2" # Additional rclone options 15 | 16 | ######################### Docker Volume Propagation ######################### 17 | 18 | source="$REPOSITORY" 19 | 20 | if [[ $source == *":"* ]]; then 21 | source_prop=":$(echo "$source" | cut -d ":" -f 2)" 22 | source=$(echo "$source" | cut -d ":" -f 1) 23 | else 24 | source_prop="" 25 | fi 26 | 27 | ################################# Rclone #################################### 28 | 29 | $NOTIFIER 30 | $NOTIFIER --message "Starting 'Rclone' job" 31 | $NOTIFIER --message "Source '$source'" 32 | $NOTIFIER --message "Remote '$REMOTE'" 33 | 34 | 35 | ################################### Backup 36 | 37 | cmd="docker run --rm --name $JOB_NAME-RcloneBackup \ 38 | --volume $HOME_PATH/Config/RcloneConfig:/config/rclone \ 39 | --volume $(dirname "$RCLONE_FILTER"):/filterfile \ 40 | --volume $HOME_PATH/LogFiles/${JOB_NAME}:/logfiles \ 41 | --volume $source:/source$source_prop \ 42 | --user $(id -u):$(id -g) \ 43 | rclone/rclone sync /source $REMOTE \ 44 | --log-file /logfiles/${JOB_NAME}_$(date +'%Y-%m').log \ 45 | --filter-from /filterfile/$(basename "$RCLONE_FILTER") \ 46 | $options" 47 | 48 | # progress message 49 | $NOTIFIER 50 | $NOTIFIER --message "Rclone in progress ... " 51 | $NOTIFIER --timestamps "false" 52 | #$NOTIFIER --message "$cmd" --timestamps "false" 53 | 54 | eval $cmd 55 | exit_code=$? 56 | 57 | ################################# Evaluation ################################ 58 | 59 | if [[ "$exit_code" == 0 ]]; then 60 | $NOTIFIER --timestamps "false" 61 | $NOTIFIER --message "Completed 'Rclone' job to '$REMOTE' successfully" 62 | exit 0 63 | else 64 | $NOTIFIER --timestamps "false" 65 | $NOTIFIER --channel "important" --type "warning" --message "ERROR $JOB_NAME: Rclone to '$REMOTE' failed with exit_code=$exit_code" 66 | $NOTIFIER --timestamps "false" 67 | exit 1 68 | fi -------------------------------------------------------------------------------- /Executor/ResticBackupExec: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # BackupScript version 2.0.0 3 | #################################### License ################################ 4 | # MIT License Copyright (c) 2023 David Krumm # 5 | # All rights reserved. # 6 | # # 7 | # This source code is licensed under the MIT license found in the # 8 | # LICENSE file in the root directory of this source tree. # 9 | ############################################################################# 10 | 11 | ################################# Parameters ################################ 12 | 13 | hostname="$1" # Set host name for detection of snapshots in the repo 14 | tags="$2" # Tags for marking snapshots 15 | options="$3" # Additional Restic options 16 | 17 | ######################### Docker Volume Propagation ######################### 18 | 19 | source="$SOURCE" 20 | repo="$REPOSITORY" 21 | 22 | if [[ $source == *":"* ]]; then 23 | source_prop=":$(echo "$source" | cut -d ":" -f 2)" 24 | source=$(echo "$source" | cut -d ":" -f 1) 25 | else 26 | source_prop="" 27 | fi 28 | 29 | if [[ $repo == *":"* ]]; then 30 | repo_prop=":$(echo "$repo" | cut -d ":" -f 2)" 31 | repo=$(echo "$repo" | cut -d ":" -f 1) 32 | else 33 | repo_prop="" 34 | fi 35 | 36 | ################################### Restic ################################## 37 | 38 | $NOTIFIER 39 | $NOTIFIER --message "Starting 'Backup' job" 40 | $NOTIFIER --message "Source '$source'" 41 | $NOTIFIER --message "Repository '$repo'" 42 | $NOTIFIER --message "Filter '$(basename "$RESTIC_FILTER")'" 43 | 44 | ################################### Backup 45 | 46 | cmd="docker run --rm --name $JOB_NAME-ResticBackup \ 47 | --hostname $hostname \ 48 | --volume $(dirname "$RESTIC_PW"):/password \ 49 | --volume $(dirname "$RESTIC_FILTER"):/filterfile \ 50 | --volume $source:/source$source_prop \ 51 | --volume $repo:/repo$repo_prop \ 52 | --user $(id -u):$(id -g) \ 53 | restic/restic \ 54 | --password-file=/password/$(basename "$RESTIC_PW") \ 55 | -r /repo backup /source $tags \ 56 | --exclude-file=/filterfile/$(basename "$RESTIC_FILTER") \ 57 | $options" 58 | 59 | # progress message 60 | $NOTIFIER 61 | $NOTIFIER --message "Backup in progress ... " 62 | $NOTIFIER --timestamps "false" 63 | #$NOTIFIER --message "$cmd" --timestamps "false" 64 | 65 | output=$(eval "$cmd" 2>&1) 66 | exit_code=$? 67 | $NOTIFIER --message "$output" --timestamps "false" 68 | 69 | ################################# Evaluation ################################ 70 | 71 | if [[ "$exit_code" == 0 ]]; then 72 | $NOTIFIER --timestamps "false" 73 | $NOTIFIER --message "Completed 'Backup' job of '$source' successfully" 74 | $NOTIFIER 75 | exit 0 76 | else 77 | $NOTIFIER --timestamps "false" 78 | $NOTIFIER --channel "important" --type "warning" --message "ERROR $JOB_NAME: Backup of '$source' failed with exit_code=$exit_code" 79 | $NOTIFIER --timestamps "false" 80 | exit 1 81 | fi -------------------------------------------------------------------------------- /Executor/Notifier: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # BackupScript version 2.0.0 3 | #################################### License ################################ 4 | # MIT License Copyright (c) 2023 David Krumm # 5 | # All rights reserved. # 6 | # # 7 | # This source code is licensed under the MIT license found in the # 8 | # LICENSE file in the root directory of this source tree. # 9 | ############################################################################# 10 | 11 | ################################# Variables ################################# 12 | 13 | timestamp="$(date +"%Y-%m-%d %H:%M:%S") - " # Log timestamp 14 | log_file="$HOME_PATH/LogFiles/${JOB_NAME}/${JOB_NAME}_$(date +'%Y-%m').log" # Path to log file 15 | mkdir -p "$(dirname "$log_file")" 16 | 17 | ############################## Parse Arguments ############################## 18 | 19 | channel="normal" # Output channel of notification 20 | type="normal" # Type of message "normal, warning" 21 | message="" # Message to send 22 | 23 | # Parse arguments 24 | while [[ $# -gt 0 ]]; do 25 | case "$1" in 26 | --channel) 27 | channel="$2" # normal or important 28 | shift 2 29 | ;; 30 | --message) 31 | message="$2" 32 | shift 2 33 | ;; 34 | --type) 35 | type="$2" # Type of message "normal, warning" 36 | shift 2 37 | ;; 38 | --timestamps) 39 | if [ "$2" == "false" ]; then # Deactivate Timestamps 40 | timestamp="" 41 | fi 42 | shift 2 43 | ;; 44 | *) 45 | echo "ERROR: $JOB_NAME Notifier unknown option: $1" 46 | echo "ERROR: $JOB_NAME Notifier unknown option: $1" >> "$log_file" 47 | exit 1 48 | ;; 49 | esac 50 | done 51 | 52 | 53 | ############################################################################# 54 | # Adjust the code below to suit your needs # 55 | ############################################################################# 56 | ################################# Interfaces ################################ 57 | 58 | to_terminal() { 59 | # Send message to log and log file 60 | local msg="$1" 61 | printf %b "$msg\n" 62 | } 63 | 64 | to_logfile() { 65 | # Send message to log and log file 66 | local msg="$1" 67 | printf %b "$msg\n" >> "$log_file" 68 | } 69 | 70 | to_unraid_notification() { 71 | # Send to Unraid notification system 72 | local severity="$1" 73 | local msg="$2" 74 | "/usr/local/emhttp/webGui/scripts/notify" -i "$severity" -e "BackupScript" -s "$JOB_NAME" -d "$msg" 75 | } 76 | 77 | to_mail() { 78 | local msg="$1" 79 | # 80 | # Implement your code 81 | # 82 | } 83 | 84 | implement_your_nofification_system() { 85 | local msg="$1" 86 | # 87 | # Implement your code 88 | # 89 | } 90 | 91 | ########################### Output Channels ################################# 92 | 93 | if [[ "$channel" == "normal" ]]; then # Message to logfile 94 | to_terminal "${timestamp}${message}" 95 | to_logfile "${timestamp}${message}" 96 | 97 | elif [[ "$channel" == "important" ]]; then # Message to notification system 98 | to_terminal "${timestamp}${message}" 99 | to_logfile "${timestamp}${message}" 100 | # to_unraid_notification "$type" "$message" # Uncomment if you use Unraid as syste 101 | # implement_your_nofification_system # Implement your code to use 102 | # to_mail # Implement your code to use 103 | fi -------------------------------------------------------------------------------- /Executor/ConditionHandler: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # BackupScript version 2.0.0 3 | #################################### License ################################ 4 | # MIT License Copyright (c) 2023 David Krumm # 5 | # All rights reserved. # 6 | # # 7 | # This source code is licensed under the MIT license found in the # 8 | # LICENSE file in the root directory of this source tree. # 9 | ############################################################################# 10 | 11 | ############################## Parse Arguments ############################## 12 | 13 | # Set default values 14 | process="" 15 | schedule="always" 16 | 17 | # Parse arguments 18 | while [[ $# -gt 0 ]]; do 19 | case "$1" in 20 | --task) 21 | task="$2" # evaluate or release 22 | shift 2 23 | ;; 24 | --process) 25 | process="$2" # name of subprocess 26 | shift 2 27 | ;; 28 | --type) 29 | type="$2" # execution, lock or pulldate 30 | shift 2 31 | ;; 32 | --schedule) 33 | schedule="$2" # Schedule for execution 34 | shift 2 35 | ;; 36 | *) 37 | $NOTIFIER --channel "important" --type "warning" --message "ERROR: $JOB_NAME ConditionHandler unknown option: $1" 38 | exit 1 39 | ;; 40 | esac 41 | done 42 | 43 | # Check for necessary arguments. 44 | if [[ -z "$task" || -z "$type" ]]; then 45 | $NOTIFIER --channel "important" --type "warning" --message "ERROR: $JOB_NAME ConditionHandler --task and --type are required" 46 | exit 1 47 | fi 48 | 49 | ################################# Variables ################################# 50 | 51 | timestamp=$(date +%Y%m%d) 52 | dir_path="$HOME_PATH/ConditionMarks" 53 | mkdir -p $dir_path 54 | if [ -z "$process" ]; then 55 | marker_file="$dir_path/${JOB_NAME}_${timestamp}.$type" 56 | else 57 | marker_file="$dir_path/${JOB_NAME}_${process}_${timestamp}.$type" 58 | fi 59 | 60 | ################################# Funktions ################################# 61 | 62 | cleanup_locks() { 63 | # Iterate through files that match the pattern filename_*.lock 64 | for file in "$dir_path"/*.$type; do 65 | # Extract the filename without the path 66 | local file_name=$(basename "$file") 67 | 68 | # Extract the date from the filename (assuming the format filename_YYYYMMDD.lock) 69 | if [[ "$file_name" =~ [0-9]{8} ]]; then 70 | file_date=${BASH_REMATCH[0]} 71 | 72 | # Compare the extracted date with today's date 73 | if [ "$file_date" != "$timestamp" ]; then 74 | # Delete the file if the date does not match today's date 75 | rm "$file" 76 | fi 77 | fi 78 | done 79 | } 80 | 81 | decode_schedule() { 82 | local sched=$1 83 | local current_date=$(date +%F) 84 | local current_day_of_week=$(date +%a) 85 | local current_day_of_month=$(date +%d) 86 | 87 | case "$sched" in 88 | "always") 89 | echo "execute" 90 | return 0 91 | ;; 92 | weekly:*) 93 | # "weekly: Mon Tue Wed Thu Fri Sat Sun" 94 | local days=${sched#weekly: } 95 | if [[ "$days" == *"$current_day_of_week"* ]]; then 96 | echo "execute" 97 | else 98 | echo "skip" 99 | fi 100 | return 0 101 | ;; 102 | monthly:*) 103 | # "monthly: 1 15" 104 | local days=${sched#monthly: } 105 | for day in $days; do 106 | if [[ "$current_day_of_month" -eq "$day" ]]; then 107 | echo "execute" 108 | return 0 109 | fi 110 | done 111 | echo "skip" 112 | return 0 113 | ;; 114 | "never") 115 | echo "skip" 116 | return 0 117 | ;; 118 | *) 119 | echo "invalid" 120 | return 0 121 | ;; 122 | esac 123 | 124 | return 1 125 | } 126 | 127 | ################################## Jobs ##################################### 128 | 129 | if [ "$task" == "evaluate" ] && [ "$type" == "execution" ]; then 130 | 131 | if [[ -f "$marker_file" ]]; then 132 | $NOTIFIER 133 | $NOTIFIER --channel "important" --type "normal" --message "$JOB_NAME: Mark for 'execution' is already set" 134 | exit 99 135 | 136 | else 137 | touch "$marker_file" 138 | $NOTIFIER 139 | $NOTIFIER --message "Create '$(basename "$marker_file")'" 140 | exit 0 141 | fi 142 | 143 | 144 | elif [ "$task" == "release" ] && [ "$type" == "execution" ]; then 145 | 146 | rm $marker_file 147 | $NOTIFIER 148 | $NOTIFIER --message "Delete Mark '$(basename "$marker_file")'" 149 | exit 0 150 | 151 | 152 | elif [ "$task" == "evaluate" ] && [ "$type" == "lock" ]; then 153 | 154 | cleanup_locks 155 | 156 | if [[ -f "$marker_file" && $schedule != "always" ]]; then 157 | $NOTIFIER 158 | $NOTIFIER --message "Mark for '$(basename "$marker_file")' is already set" 159 | exit 99 160 | fi 161 | 162 | result=$(decode_schedule "$schedule") 163 | 164 | if [[ $result == "execute" ]] ; then 165 | touch "$marker_file" 166 | $NOTIFIER 167 | $NOTIFIER --message "Create '$(basename "$marker_file")'" 168 | exit 0 169 | 170 | elif [[ $result == "skip" ]]; then 171 | $NOTIFIER 172 | $NOTIFIER --message "Skipped '$process' due to the schedule '$schedule'" 173 | exit 99 174 | elif [[ $result == "invalid" ]]; then 175 | $NOTIFIER --channel "important" --type "warning" --message "ERROR: $process schedule '$schedule' is no valid option" 176 | $NOTIFIER --message "Here are some examples of valid options:" 177 | $NOTIFIER --message "'always'" 178 | $NOTIFIER --message "'weekly: Mon, Tue, Thu, Sat'" 179 | $NOTIFIER --message "'monthly: 12'" 180 | $NOTIFIER --message "'never'" 181 | exit 1 182 | else 183 | $NOTIFIER --channel "important" --type "warning" --message "ERROR: $JOB_NAME decode schedule '$schedule' failed" 184 | exit 1 185 | fi 186 | 187 | 188 | else 189 | $NOTIFIER --channel "important" --type "warning" --message "ERROR: $JOB_NAME ConditionHandler command unknown" 190 | exit 1 191 | fi 192 | -------------------------------------------------------------------------------- /Jobs/JobTemplate: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # BackupScript version 2.0.0 3 | #################################### License #################################### 4 | # MIT License Copyright (c) 2023 David Krumm # 5 | # All rights reserved. # 6 | # # 7 | # This source code is licensed under the MIT license found in the # 8 | # LICENSE file in the root directory of this source tree. # 9 | ################################################################################# 10 | # BackupScript # 11 | ################################################################################# 12 | # 13 | unique_job_name="ChangeMeToUniqueName" # Unique job name. Do not use space or underscore!!! 14 | # 15 | system_id_name="HOSTNAME" # Name to identify your System in Snapshots. 16 | # 17 | schedule_update_Restic_and_Rclone="monthly: 1" # Schedule for the execution of the respective sub-process: 18 | # "never", "always", "weekly: Mon Tue Wed Thu Fri Sat Sun", "monthly: 1 7 14 21 28" 19 | # Executed only once per day if the weekly or monthly pattern is used. 20 | # 21 | notify_after_completion="true" # (true/false) The notification system must be set up. See documentation. 22 | # 23 | ############################## Backup (Restic) ################################## 24 | # 25 | schedule_backup="always" # 26 | # 27 | path_to_the_directory_to_be_backed_up="/PATH/TO/DATA:ro" # Source directory to be backed up. Support for Docker volume propagation! E.G. "/PATH/TO/DATA:rw,slave" ro=read-only, rw=read-write 28 | # 29 | path_to_restic_repository="/PATH/TO/REPO:rw,slave" # Path to the backup repository. Support for Docker volume propagation! E.G. "/PATH/TO/REPO:rw,slave" ro=read-only, rw=read-write 30 | # 31 | name_restic_password_file="restic-repo.password" # Name of a password file in the path "BackupScript > Config > RepositoryPassword". 32 | # 33 | restic_backup_tags="--tag FirstTag --tag SecondTag" # Tags to be applied to the backup snapshots. 34 | # 35 | name_restic_filter_file="DefaultResticFilter.txt" # Name of a configuration file in the path "BackupScript > Config > ResticFilter". 36 | # 37 | restic_options="" # Additional options specific to Restic. Check with the documentation. 38 | # 39 | name_docker_config_file="DefaultDockerConfig.txt" # Name of a configuration file in the path "BackupScript > Config > DockerConfig". 40 | # 41 | reverse_docker_start_sequence="false" # (true/false) Option to reverse the order of the Docker containers when starting. 42 | # 43 | ############################## Forget (Restic) ################################## 44 | # 45 | schedule_forget="weekly: Mon Wed Sat" # 46 | # 47 | keep_hourly_for="48h" # Number of hours to keep hourly snapshots. 48 | keep_daily_for="7d" # Number of days to keep daily snapshots. 49 | keep_weekly_for="3m" # Number of weeks to keep weekly snapshots. 50 | keep_monthly_for="1y" # Number of months to keep monthly snapshots. 51 | keep_yearly_for="5y" # Number of years to keep yearly snapshots. 52 | # 53 | ############################## Prune (Restic) ################################### 54 | # 55 | schedule_prune="monthly: 1 15" # 56 | # 57 | ############################## Remote Copy (Rclone) ############################# 58 | # 59 | schedule_rclone="weekly: The Sun" # 60 | # 61 | rclone_remote_path="RemoteName:/PATH/ON/REMOTE" # Remote destination for Rclone. Does not support Docker volume propagation! 62 | # 63 | name_rclone_filter_file="DefaultRcloneFilter.txt" # Name of a configuration file in the path "BackupScript > Config > RcloneFilter". 64 | # 65 | rclone_options="--log-level INFO" # Additional options specific to Rclone. Check with the documentation. 66 | # Example: "--dry-run" "--max-delete=50" "--bwlimit 10M" "--log-level DEBUG". 67 | # 68 | ################################################################################# 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | ################################################################################# 101 | # Don't change the following lines # 102 | ################################################################################# 103 | path_to_BackupScript=$(dirname "$(dirname "$(readlink -f "$0")")") 104 | error_message="\ 105 | !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! ERROR !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!\n\ 106 | \n\ 107 | Project 'BackupsScript' was set up incorrectly.\n\ 108 | Please check the setup by following the instructions.\n\ 109 | This path '$path_to_BackupScript' should point to the root directory of BackupScript.\n\ 110 | \n\ 111 | Compare:\n\ 112 | \n\ 113 | └── BackupScript\n\ 114 | ├── Config\n\ 115 | │ ├── DockerConfig\n\ 116 | │ ├── FilterConfig\n\ 117 | │ ├── RcloneConfig\n\ 118 | │ └── RepositoryPassword\n\ 119 | ├── Executor\n\ 120 | ├── Jobs\n\ 121 | │ └── YourBackupJob\n\ 122 | └── SetupInstructions\n\ 123 | \n\ 124 | !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! ERROR !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!\n\ 125 | " 126 | if [ ! -f "$path_to_BackupScript/Executor/MainExec" ]; then 127 | printf "%b" "$error_message" 128 | exit 1 129 | fi 130 | source $path_to_BackupScript/Executor/MainExec 131 | ################################################################################# -------------------------------------------------------------------------------- /Executor/MainExec: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # BackupScript version 2.0.0 3 | #################################### License #################################### 4 | # MIT License Copyright (c) 2023 David Krumm # 5 | # All rights reserved. # 6 | # # 7 | # This source code is licensed under the MIT license found in the # 8 | # LICENSE file in the root directory of this source tree. # 9 | ################################################################################# 10 | # # 11 | # BackupScript # 12 | # # 13 | ################################################################################# 14 | 15 | ############################# Setup Paths ####################################### 16 | 17 | export JOB_NAME="$unique_job_name" 18 | export HOME_PATH="$path_to_BackupScript" 19 | export SOURCE="$path_to_the_directory_to_be_backed_up" 20 | export REPOSITORY="$path_to_restic_repository" 21 | export REMOTE="$rclone_remote_path" 22 | 23 | export DOCKER_SHUTDOWN_CONFIG="$HOME_PATH/Config/DockerConfig/$name_docker_config_file" 24 | export RESTIC_PW="$HOME_PATH/Config/RepositoryPassword/$name_restic_password_file" 25 | export RESTIC_FILTER="$HOME_PATH/Config/FilterConfig/ResticFilter/$name_restic_filter_file" 26 | export RCLONE_FILTER="$HOME_PATH/Config/FilterConfig/RcloneFilter/$name_rclone_filter_file" 27 | 28 | export NOTIFIER="$HOME_PATH/Executor/Notifier" 29 | ConditionHandler="$HOME_PATH/Executor/ConditionHandler" 30 | DockerHandler="$HOME_PATH/Executor/DockerHandler" 31 | ResticBackupExec="$HOME_PATH/Executor/ResticBackupExec" 32 | ResticForgetExec="$HOME_PATH/Executor/ResticForgetExec" 33 | ResticPruneExec="$HOME_PATH/Executor/ResticPruneExec" 34 | RcloneExec="$HOME_PATH/Executor/RcloneExec" 35 | 36 | ############################## Check Preconditions ############################## 37 | 38 | directory_pahts=("$HOME_PATH" "$SOURCE" "$REPOSITORY") 39 | config_files=("$DOCKER_SHUTDOWN_CONFIG" "$RESTIC_PW" "$RESTIC_FILTER" "$RCLONE_FILTER") 40 | executable_files=("$NOTIFIER" "$ConditionHandler" "$DockerHandler" "$ResticBackupExec" "$ResticForgetExec" "$ResticPruneExec" "$RcloneExec") 41 | 42 | for directory in "${directory_pahts[@]}"; do 43 | directory=$(echo "$directory" | cut -d':' -f1) 44 | 45 | if [ ! -d "$directory" ]; then 46 | echo "ERROR: The directory $directory does not exist." 47 | exit 1 48 | fi 49 | done 50 | 51 | for file in "${config_files[@]}"; do 52 | 53 | if [ ! -f "$file" ]; then 54 | echo "ERROR: The file $file does not exist." 55 | exit 1 56 | 57 | 58 | elif [ "$(tail -c 1 "$file"; echo x)" != "$(echo x)" ] && [ -n "$(tail -n 1 "$file")" ]; then 59 | echo >> "$file" 60 | fi 61 | done 62 | 63 | for file in "${executable_files[@]}"; do 64 | 65 | if [ ! -f "$file" ]; then 66 | echo "ERROR: $file does not exist" 67 | exit 1 68 | 69 | elif [ ! -x "$file" ]; then 70 | echo "ERROR: $file exists but is not executable" 71 | 72 | chmod +x "$file" 73 | 74 | if [ $? -eq 0 ]; then 75 | echo "-> $file was provided with execution rights" 76 | else 77 | echo "ERROR: The attempt to assign execution rights to the $file failed." 78 | echo "-> Read the instructions and the setup files in the 'BackupScript/SetupInstruction' directory" 79 | echo "-> Use 'sudo chmod +x $file' to give the file the required rights" 80 | exit 1 81 | fi 82 | fi 83 | done 84 | 85 | ############################## Jobs ############################################# 86 | 87 | exec_backup=" " 88 | exec_forget=" " 89 | exec_prune=" " 90 | exec_rclone=" " 91 | 92 | $NOTIFIER --timestamps "false" --message \ 93 | "\n\ 94 | ####################################################################################################\n\ 95 | \tStarting '$JOB_NAME'\n\ 96 | \tDate: $(date +"%Y-%m-%d")\n\ 97 | \tTime: $(date +"%H:%M:%S")\n\ 98 | ####################################################################################################" 99 | 100 | 101 | $ConditionHandler --task "evaluate" --type "execution" 102 | job_is_already_executed=$? 103 | if [[ "$job_is_already_executed" == 99 ]]; then 104 | exit 0 105 | elif [[ "$job_is_already_executed" == 1 ]]; then 106 | exit 1 107 | fi 108 | 109 | ############################## Update Restic & Rclone 110 | 111 | $ConditionHandler --task "evaluate" --type "lock" --process "Update" --schedule "$schedule_update_Restic_and_Rclone" 112 | evaluation=$? 113 | 114 | if [[ "$evaluation" == 0 ]]; then 115 | $DockerHandler --action "update" 116 | fi 117 | 118 | ############################## Backup 119 | 120 | $ConditionHandler --task "evaluate" --type "lock" --process "Backup" --schedule "$schedule_backup" 121 | evaluation=$? 122 | 123 | ############### Stop Docker 124 | 125 | if [[ "$evaluation" == 0 ]]; then 126 | $DockerHandler --action "stop" 127 | stop_exit_code=$? 128 | fi 129 | 130 | if [[ "$evaluation" == 1 || "$stop_exit_code" == 1 ]]; then 131 | $ConditionHandler --task "release" --type "execution" 132 | exit 1 133 | fi 134 | 135 | ############### Backup 136 | 137 | if [[ "$evaluation" == 0 ]]; then 138 | $ResticBackupExec "$system_id_name" "$restic_backup_tags" "$restic_options" 139 | backup_exit_code=$? 140 | exec_backup="X" 141 | fi 142 | 143 | if [[ "$backup_exit_code" == 1 ]]; then 144 | $ConditionHandler --task "release" --type "execution" 145 | exit 1 146 | fi 147 | 148 | ############### Start Docker 149 | 150 | if [[ "$evaluation" == 0 ]]; then 151 | $DockerHandler --action "start" --reverse "$reverse_docker_start_sequence" 152 | start_exit_code=$? 153 | fi 154 | 155 | if [[ "$start_exit_code" == 1 ]]; then 156 | $ConditionHandler --task "release" --type "execution" 157 | exit 1 158 | fi 159 | 160 | ############################## Forget 161 | 162 | keep_rules="--keep-within-hourly $keep_hourly_for --keep-within-daily $keep_daily_for" 163 | keep_rules+=" --keep-within-weekly $keep_weekly_for --keep-within-monthly $keep_monthly_for" 164 | keep_rules+=" --keep-within-yearly $keep_yearly_for" 165 | 166 | $ConditionHandler --task "evaluate" --type "lock" --process "Forget" --schedule "$schedule_forget" 167 | evaluation=$? 168 | 169 | if [[ "$evaluation" == 0 ]]; then 170 | $ResticForgetExec "$keep_rules" 171 | forget_exit_code=$? 172 | exec_forget="X" 173 | fi 174 | 175 | if [[ "$evaluation" == 1 || "$forget_exit_code" == 1 ]]; then 176 | $ConditionHandler --task "release" --type "execution" 177 | exit 1 178 | fi 179 | 180 | ############################## Prune 181 | 182 | $ConditionHandler --task "evaluate" --type "lock" --process "Prune" --schedule "$schedule_prune" 183 | evaluation=$? 184 | 185 | if [[ "$evaluation" == 0 ]]; then 186 | $ResticPruneExec 187 | prune_exit_code=$? 188 | exec_prune="X" 189 | fi 190 | 191 | if [[ "$evaluation" == 1 || "$prune_exit_code" == 1 ]]; then 192 | $ConditionHandler --task "release" --type "execution" 193 | exit 1 194 | fi 195 | 196 | ############################## Rclone 197 | 198 | $ConditionHandler --task "evaluate" --type "lock" --process "Rclone" --schedule "$schedule_rclone" 199 | evaluation=$? 200 | 201 | if [[ "$evaluation" == 0 ]]; then 202 | $RcloneExec "--log-file /LogFiles/${JOB_NAME}_$(date +'%Y-%m').log" "$rclone_options" 203 | rclone_exit_code=$? 204 | exec_rclone="X" 205 | fi 206 | 207 | if [[ "$evaluation" == 1 || "$rclone_exit_code" == 1 ]]; then 208 | $ConditionHandler --task "release" --type "execution" 209 | exit 1 210 | fi 211 | 212 | ############################## Finished 213 | 214 | $ConditionHandler --task "release" --type "execution" 215 | 216 | completion_channel="normal" 217 | if [[ "$notify_after_completion" = "true" ]]; then 218 | completion_channel="important" 219 | fi 220 | 221 | $NOTIFIER --timestamps "false" --message \ 222 | "\n\ 223 | ####################################################################################################" 224 | $NOTIFIER --channel "$completion_channel" --timestamps "false" --message \ 225 | "Execution of '$JOB_NAME' successfully finished.\n\n\ 226 | Executed parts:\n\ 227 | \t[$exec_backup] Backup\n\ 228 | \t[$exec_forget] Forget\n\ 229 | \t[$exec_prune] Prune\n\ 230 | \t[$exec_rclone] Rclone" 231 | $NOTIFIER --timestamps "false" --message \ 232 | "####################################################################################################\n" 233 | 234 | ################################################################################# 235 | # End # 236 | ################################################################################# -------------------------------------------------------------------------------- /Executor/DockerHandler: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # BackupScript version 2.0.0 3 | #################################### License ################################ 4 | # MIT License Copyright (c) 2023 David Krumm # 5 | # All rights reserved. # 6 | # # 7 | # This source code is licensed under the MIT license found in the # 8 | # LICENSE file in the root directory of this source tree. # 9 | ############################################################################# 10 | 11 | ############################## Parse Arguments ############################## 12 | 13 | # Default values 14 | reverse="false" 15 | 16 | # Parse arguments 17 | while [[ $# -gt 0 ]]; do 18 | case "$1" in 19 | --action) 20 | action="$2" # start, stop, update 21 | shift 2 22 | ;; 23 | --reverse) 24 | reverse="$2" # reverse order on start 25 | shift 2 26 | ;; 27 | *) 28 | $NOTIFIER --channel "important" --type "warning" --message "ERROR: $JOB_NAME DockerHandler unknown option: $1" 29 | exit 1 30 | ;; 31 | esac 32 | done 33 | 34 | # Check for necessary arguments 35 | if [[ -z "$action" ]]; then 36 | $NOTIFIER --channel "important" --type "warning" --message "ERROR: $JOB_NAME DockerHandler --action is required" 37 | exit 1 38 | fi 39 | 40 | # Variablen definieren 41 | timeout=60 42 | dir_path="$HOME_PATH/ConditionMarks" 43 | mkdir -p "$dir_path" 44 | started_containers="$dir_path/${JOB_NAME}_Started_Container.temp" 45 | stoped_containers="$dir_path/${JOB_NAME}_Stoped_Container.temp" 46 | 47 | ################################# Funktions ################################# 48 | 49 | pull_image() { 50 | local image_name=$1 51 | pull_output=$(docker pull "$image_name" 2>&1) 52 | 53 | if [ $? -ne 0 ]; then 54 | return 1 55 | fi 56 | 57 | if echo "$pull_output" | grep -q 'Downloaded newer image'; then 58 | $NOTIFIER --message "$JOB_NAME: The image '$image_name' has been successfully updated." 59 | elif echo "$pull_output" | grep -q 'Image is up to date'; then 60 | $NOTIFIER --message "$JOB_NAME: The local image '$image_name' is already the latest version." 61 | else 62 | $NOTIFIER --message "$JOB_NAME: Unknown status when the image '$image_name' is being pulled: $pull_output" 63 | fi 64 | } 65 | 66 | container_status() { 67 | docker inspect -f '{{.State.Status}}' "$1" 2>/dev/null 68 | } 69 | 70 | list_started_containers() { 71 | if [ -f "$started_containers" ]; then 72 | rm "$started_containers" 73 | fi 74 | 75 | docker ps --format "{{.ID}} {{.Names}}" > "$started_containers" 76 | 77 | if [ $? -ne 0 ]; then 78 | $NOTIFIER --channel "important" --type "warning" --message "$JOB_NAME: Failed to query started containers" 79 | exit 1 80 | fi 81 | } 82 | 83 | trim() { 84 | local var="$1" 85 | var="${var#"${var%%[![:space:]]*}"}" # Remove leading space 86 | var="${var%"${var##*[![:space:]]}"}" # Remove trailing spaces 87 | echo -n "$var" 88 | } 89 | 90 | start_container() { 91 | local id=$1 92 | local name=$2 93 | docker start "$id" > /dev/null 2>&1 94 | local start_time=$(date +%s) 95 | sleep 2 96 | while [[ "$(container_status "$id")" != "running" ]]; do 97 | sleep 1 98 | if (( $(date +%s) - start_time >= timeout )); then 99 | $NOTIFIER --channel "important" --message "Error: $JOB_NAME timeout while starting container $name ($id)" 100 | exit 1 101 | fi 102 | done 103 | $NOTIFIER --message "Container $name ($id) started successfully." 104 | return 0 105 | } 106 | 107 | stop_container() { 108 | local id=$1 109 | local name=$2 110 | docker stop "$id" > /dev/null 2>&1 111 | local start_time=$(date +%s) 112 | sleep 2 113 | while [[ "$(container_status "$id")" != "exited" ]]; do 114 | sleep 1 115 | if (( $(date +%s) - start_time >= timeout )); then 116 | $NOTIFIER --channel "important" --message "Error: $JOB_NAME timeout while stoping container $name ($id)" 117 | exit 1 118 | fi 119 | done 120 | $NOTIFIER --message "Container $name ($id) stoped successfully." 121 | return 0 122 | } 123 | 124 | decode_config() { 125 | 126 | included_pattern=() 127 | exclude_pattern=() 128 | stop_remaining="false" 129 | 130 | 131 | while IFS= read -r line; do 132 | local flag=$(trim "$(echo "$line" | awk '{print $1}')") 133 | local pattern=$(trim "$(echo "$line" | awk '{print $2}')") 134 | 135 | if [[ $flag == "+" ]]; then 136 | 137 | if [[ "$pattern" == "*" ]]; then 138 | stop_remaining="true" 139 | else 140 | included_pattern+=("$pattern") 141 | fi 142 | 143 | elif [[ $flag == "-" ]]; then 144 | exclude_pattern+=("$pattern") 145 | fi 146 | 147 | done < "$DOCKER_SHUTDOWN_CONFIG" 148 | 149 | } 150 | 151 | process_remaining() { 152 | 153 | list_started_containers 154 | 155 | while IFS= read -r line; do 156 | local id=$(trim "$(echo "$line" | awk '{print $1}')") 157 | local name=$(trim "$(echo "$line" | awk '{print $2}')") 158 | local execute_stop="true" 159 | 160 | for pattern in "${exclude_pattern[@]}"; do 161 | 162 | pattern=$(trim "$pattern") 163 | 164 | if [[ "$id" == $pattern || "$name" == $pattern ]]; then 165 | execute_stop="false" 166 | break 167 | fi 168 | done 169 | 170 | if [[ "$execute_stop" == "true" ]]; then 171 | stop_container "$id" "$name" 172 | echo "$id $name" >> "$stoped_containers" 173 | fi 174 | done < "$started_containers" 175 | 176 | rm "$started_containers" 177 | } 178 | 179 | process_stop() { 180 | 181 | if [ -f "$stoped_containers" ]; then 182 | rm "$stoped_containers" 183 | fi 184 | 185 | touch $stoped_containers 186 | list_started_containers 187 | 188 | local started_containers_id=() 189 | local started_containers_name=() 190 | local stopped_flags=() 191 | 192 | while IFS= read -r line; do 193 | local id=$(trim "$(echo "$line" | awk '{print $1}')") 194 | local name=$(trim "$(echo "$line" | awk '{print $2}')") 195 | 196 | started_containers_id+=("$id") 197 | started_containers_name+=("$name") 198 | stopped_flags+=("false") 199 | 200 | done < "$started_containers" 201 | 202 | for pattern in "${included_pattern[@]}"; do 203 | 204 | pattern=$(trim "$pattern") 205 | 206 | for i in "${!started_containers_id[@]}"; do 207 | 208 | if [[ ${stopped_flags[$i]} == "true" ]]; then 209 | continue 210 | fi 211 | 212 | local id="${started_containers_id[$i]}" 213 | local name="${started_containers_name[$i]}" 214 | 215 | if [[ "$id" == $pattern || "$name" == $pattern ]]; then 216 | stop_container "$id" "$name" 217 | echo "$id $name" >> "$stoped_containers" 218 | stopped_flags[$i]="true" 219 | break 220 | fi 221 | 222 | done 223 | done 224 | 225 | rm "$started_containers" 226 | 227 | if [[ $stop_remaining == "true" ]]; then 228 | process_remaining 229 | fi 230 | } 231 | 232 | process_start() { 233 | 234 | if [ ! -f "$stoped_containers" ]; then 235 | $NOTIFIER --channel "important" --type "warning" --message "$JOB_NAME: Start containers faild" 236 | $NOTIFIER --message "Temporary file of the stopped container was lost" 237 | exit 1 238 | fi 239 | 240 | local ids_to_start=() 241 | local names_to_start=() 242 | 243 | while IFS= read -r line; do 244 | local id=$(trim "$(echo "$line" | awk '{print $1}')") 245 | local name=$(trim "$(echo "$line" | awk '{print $2}')") 246 | 247 | ids_to_start+=("$id") 248 | names_to_start+=("$name") 249 | 250 | done < "$stoped_containers" 251 | 252 | if [[ "$reverse" == "true" ]]; then 253 | ids_to_start=($(echo "${ids_to_start[@]}" | tac -s ' ')) 254 | names_to_start=($(echo "${names_to_start[@]}" | tac -s ' ')) 255 | fi 256 | 257 | for i in "${!ids_to_start[@]}"; do 258 | start_container "${ids_to_start[$i]}" "${names_to_start[$i]}" 259 | done 260 | 261 | rm "$stoped_containers" 262 | 263 | } 264 | 265 | ################################## Jobs ##################################### 266 | 267 | if [[ "$action" == "update" ]]; then 268 | 269 | $NOTIFIER 270 | $NOTIFIER --message "Starting 'Update' job" 271 | $NOTIFIER 272 | 273 | pull_image "restic/restic" 274 | update_restic_result=$? 275 | pull_image "rclone/rclone" 276 | update_rclone_result=$? 277 | 278 | if [[ "$update_restic_result" == 1 || "$update_rclone_result" == 1 ]]; then 279 | $NOTIFIER --channel "important" --type "warning" --message "$JOB_NAME: Pull or update of Restic or Rclone failed." 280 | $NOTIFIER --message "Restic exit code: '$update_restic_result'" 281 | $NOTIFIER --message "Rclone exit code: '$update_rclone_result'" 282 | $NOTIFIER --message "An attempt is made to continue." 283 | exit 0 284 | fi 285 | 286 | 287 | elif [[ "$action" == "stop" ]]; then 288 | 289 | $NOTIFIER 290 | $NOTIFIER --message "Stopping the Docker containers" 291 | $NOTIFIER --message "Config '$(basename "$DOCKER_SHUTDOWN_CONFIG")'" 292 | $NOTIFIER 293 | 294 | decode_config 295 | process_stop 296 | exit 0 297 | 298 | 299 | elif [[ "$action" == "start" ]]; then 300 | 301 | process_start 302 | exit 0 303 | 304 | 305 | else 306 | $NOTIFIER --channel "important" --type "warning" --message "ERROR: $JOB_NAME DockerHandler unknown action: $action" 307 | exit 1 308 | fi 309 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # BackupScript with Restic and Rclone 2 | 3 | ## Introduction 4 | 5 | Welcome to the BackupScripts Repository! This small private project aims to create fast, easy and reliable backups. After setup, all you need to do for this is to customize a template for the desired backup job. The powerful tools Restic and Rclone are used for the backup process. Since the official Docker containers of Restic and Rclone are installed automatically, no additional installation is required. All that is needed is a Unix system (Linux or macOS) on which Docker containers can be run. 6 | 7 | [Restic](https://restic.net/) is an open source backup utility to protect your data by safely storing and restoring incremental backups. It is easy to use, efficient and reliable and offers a straightforward and flexible solution for backing up and restoring data. With Restic, you can manage your backup process, encrypt your data, store it and ensure the security and accessibility of your important files. 8 | 9 | [Rclone](https://rclone.org/) is a versatile command line programme that allows you to synchronise files and directories between different storage locations. Its main purpose is to provide a unified interface for managing and transferring data between different storage platforms such as Google Drive, Dropbox, Amazon S3 or your own remote server. Rclone provides the ability to copy, move and synchronise files, as well as perform advanced operations such as encryption, deduplication and bandwidth control, making it a powerful tool for managing cloud storage. 10 | 11 | The combination of Restic and Rclone provides a flexible and robust solution for performing backups that ensures the security and integrity of your data. The scripts provided in this repository simplify the configuration and execution of backup operations and make it easy for you to set up and maintain a reliable backup system. 12 | 13 | Only basic command line knowledge is required for use and installation. The automation of the individually created backup jobs can be realised via cron jobs or similar. The scripts are not connected to Restic or Rclone. No liability is assumed for the use or loss of data. 14 | 15 | ### Features 16 | - Local and remote backups: The scripts support both local and remote backup scenarios. You can choose to back up your data only to a local directory or use different cloud storage providers supported by Rclone. 17 | - Incremental backups: Restic performs efficient incremental backups by identifying changes in files, minimising storage requirements and network bandwidth usage. 18 | - Encryption and security: Restic encrypts your data before it is stored, ensuring privacy and security. If desired, Rclone supports further encryption on the remote system. 19 | - Configurable retention policies: You can customise retention policies to determine how long backups should be kept, balancing storage usage with backup history. 20 | - Docker handeling: The script allows different docker containers to be stopped and then restarted in a defined order for the backup. 21 | - Notification support: Scripts are provided with support for Unraid's notification system. Deviating solutions must be implemented by the user. 22 | 23 | ## Table of Contents 24 | 25 | 1. [Project Description](#project-description) 26 | 2. [Installation](#installation) 27 | 3. [Usage](#usage) 28 | 4. [Final Note](#final-note) 29 | 4. [License](#license) 30 | 31 | ## Project Description 32 | 33 | The BackupScript project is a set of bash scripts designed to manage local and remote backups with the Restic and Rclone tools. Its main purpose is to provide a flexible and customisable solution for performing incremental backups, rotating snapshots and moving backups to a remote system. 34 | 35 | The script is structured with a job template. By customising this template and defining values such as the source directory for the backup, the path to the backup repository, the password file for the repository us, multiple backup jobs can be created easily and flexibly. Since the official Docker containers of the respective projects are used to run Rclone and Restic, no direct installation on the system is necessary. 36 | 37 | The script offers functions for handling Docker containers during the backup, so that containers are stopped in a defined order and then restarted. By scheduling the created backup jobs with cron jobs or similar solutions, the entire backup process can be automated. 38 | 39 | ## Installation 40 | 41 | To install and set up the project, please follow the steps below: 42 | 43 | ### Requirements ### 44 | 45 | - **Operating System** 46 | - The script is designed to run on a Unix-like operating system such as Linux or macOS. 47 | 48 | - **Dependencies** 49 | - Docker: The host system should have Docker installed and properly configured to run containers. 50 | - Restic and Rclone: The official Docker images [Restic](https://hub.docker.com/r/restic/restic) and [Rclone](https://hub.docker.com/r/rclone/rclone) are used for execution and installed automatically. Ensure that the system has access to the Internet. Otherwise, the images must be deployed locally. 51 | 52 | - **Setup/Job Configuration** 53 | - Read the instructions to ensure the correct setup and function of backup scripts. 54 | - It is helpful if you inform yourself about the basic functions of Restic and Rclone in order to gain a better understanding of the function and structure of backup scripts. 55 | - Restic and Rclone are command line programs. Detailed instructions are provided for their use. However, there is no graphical user interface. 56 | - As Docker containers are used, you should have basic knowledge of working with Docker containers. 57 | 58 | - **Access and Permissions** 59 | - You should have sufficient access rights on the system to run the script and access the required directories, files, and Docker resources. 60 | 61 | - **Notification Setup** 62 | - The script offers the option of sending notifications. This is already prepared for Unraid. Other output sources (e.g. e-mail) must be implemented yourself. 63 | 64 | - **Script Execution/Scheduling** 65 | - The use of backup scripts requires the execution of bash scripts. Detailed instructions are provided for this. 66 | - To automate backup tasks, the execution of the backup job must be scheduled. Under Unraid, the "User Scripts" application can be used for this purpose. Alternatively, a cron job can be created directly under Linux systems. Detailed instructions are provided for this. 67 | 68 | ### **1. Download the repository** ### 69 | - Download the entire repository and place all the directories and files it contains in a new directory. The name "BackupScript" is recommended. If "BackupScript" is not selected as the name of the root directory, this must be taken into account in the Docker commands during setup. The resulting folder structure is shown below. Do not change the name or structure of the repository content unless you are explicitly requested to do so in the instructions. Place the filled "BackupScript" directory in a suitable location of your choice on your system. 70 | 71 | ``` 72 | └── BackupScript 73 | ├── Config 74 | │ ├── DockerConfig 75 | │ ├── FilterConfig 76 | │ ├── RcloneConfig 77 | │ └── RepositoryPassword 78 | ├── Executor 79 | ├── Jobs 80 | └── SetupInstructions 81 | ``` 82 | 83 | ### **2. Setup backup repository** ### 84 | - A repository must be created before use. This is where the incremental backups are stored. You can add all backup jobs to the same repository or create a separate repository for each job. Restic [documentation](https://restic.readthedocs.io/en/latest/) is used to create the backups. To create a repository, follow the instructions in "BackupScript > SetupInstruction > Create_Repository.txt". 85 | 86 | ### **3. (Optional) Setup remote system** ### 87 | - To increase the security of the backups, the repository can be transferred to a remote system or cloud. Rclone [documentation](https://rclone.org/docs/) is used for this purpose. Rclone offers a variety of options for connecting to a remote system or cloud. Before using a new connection for the first time, it must be configured according to the Rclone [documentation](https://rclone.org/docs/#configure). If no remote backups are required, this step can be skipped. However, the schedule for Rclone in the backup job must then be set to "never". If you have decided to use a remote system, you can start the interactive setup process by following the instructions in "BackupScript > SetupInstruction > Create_Rclone_Config.txt". All information required for the setup process is provided by the Rclone documentation. Adding new connections or changing existing ones is possible at any time. 88 | 89 | ### **4. Setup backup job** ### 90 | - New backup jobs are created by configuring a job. A pre-filled template is provided for this purpose. Create a copy of the "JobTemplate" file in the "BackupScript > Jobs" directory and give it a unique name (avoid using spaces and underscores). A description of the configuration of new jobs is given in the section [Usage](#usage). By copying the template, any number of jobs with different configurations can be created. 91 | 92 | ### **5. (Optional) Setup notification** ### 93 | - If important events occur during the execution of a script, a notification can be sent. This is already prepared for the unraid notification system. To activate this function, the script "BackupScript > Executor > Notifier" must be opened with an editor. Activation is done by removing the "#" at the beginning of line 100. "Notifier" is designed so that further interfaces for notifications can be added as functions. Implement the interfaces required for your needs and add the methods in the desired "Output Channel". 94 | 95 | ## Usage 96 | 97 | ### Setting up a backup job ### 98 | 99 | Once the setup is complete, any number of backup jobs can be created. To quickly and easily create a backup job, a job template is provided. The file "JobTemplate" can be found under "BackupScript > Jobs". By creating a copy, the template can be customized to the desired backup job. For this purpose, a number of variables are available that can be used to configure the backup job. Please do not change the names of the variables or the range under them. It is also not necessary to change the files in the executor directory, with the exception of "Notifier" for setting up custom notification interfaces. 100 | 101 | A backup job consists of a total of four sub-processes: 102 | 1. **Backup (Restic)** In this step, the source directory is scanned and a new snapshot is added to the repository. As in some cases Docker containers have to be stopped for a backup (e.g. when backing up a database), they can be stopped during the backup using a configuration file and then restarted. 103 | 2. **Forget (Restic)**: Snapshots can be given a shelf life. If this is exceeded, the snapshots in question are removed from the repository. Retention guidelines are available for this purpose. During the "Forget" process, these policies are applied to the snapshots in the repository and removed. 104 | 3. **Prune (Restic)**: When "forgetting", only the incremental links are removed, but no data is deleted from the repository. This is achieved by running "Prune". This searches for files that are not linked to any existing snapshot and can therefore be deleted. 105 | 4. **Remote Copy (Rclone)**: Rclone is used to transfer the local repository to a remote system. In this process, the selected Rclone configuration is used and the repository is transferred. The script uses the "sync" mode of Rclone. Therefore, files that are no longer present in the local repository are also removed from the remote repository. 106 | 107 | The sub-processes "Backup", "Forget", "Prune" and "Remote Copy" have their own schedule. This is independent of the job's execution schedule. Regardless of how often the job is executed, the respective sub-process is only executed if this is provided for by the respective schedule. Sub-processes can be executed "never", "always", on certain days of the week "weekly" or on certain days of the month "monthly". This means that "weekly: Mon Thu Sat" is executed on the weekdays Monday, Thursday and Saturday. In contrast, "monthly: 7 21 28" is executed on the respective calendar days of a month. If the "weekly" or "monthly" pattern is used, the sub-process is only executed once on the day in question. This makes it possible, for example, to create a local backup every 5 minutes by executing the job at this interval and setting the schedule for "Backup" to "always", but only copying to the remote system on certain days using a "weekly" schedule. To prevent re-execution, "Lock files" are created under the path "BackupScript > ConditionMarks" with the designation of the job name and the sub-process. If a sub-process is to be executed a second time, the schedule can be set to "always" for a short time or the relevant lock file must be deleted manually from the "ConditionMarks" directory. Obsolete lock files are deleted automatically 108 | 109 | An overview of all the variables available is listed below with descriptions. It is recommended that you read about the variables before using this script. In all other cases, a look at the documentation of Restic and Rclone will also help. 110 | 111 | ### Job Configuration ### 112 | 113 | The most important variables to configure are: 114 | 115 | - `unique_job_name`: The name of the script or backup job. Must be unique as it is used to recognise whether the backup task is already running. Avoid spaces or underscores! 116 | - `system_id_name`: The name used to identify your system in the snapshots. 117 | - `schedule_update_Restic_and_Rclone`: Schedule for the search for updates for Restic and Rclone. Examples: "never" "always" "weekly: Mon Thu Sat" "monthly: 7 21 28" 118 | - `notify_after_completion`: Option to send a notification after the job has been successfully executed. Options: "true" "false" 119 | - `schedule_backup`: Schedule for the execution of the "Backup" sub-process. Examples: "never" "always" "weekly: Mon Thu Sat" "monthly: 7 21 28" 120 | - `path_to_the_directory_to_be_backed_up`: The source directory to be backed up. Support for [Docker volume propagation](https://docs.docker.com/storage/bind-mounts/)! E.G. "/PATH/TO/DIRECTORY:rw,slave". By using "ro", access can be restricted to read only. 121 | - `path_to_restic_repository`: The path to the backup repository where the backups will be stored. Support for [Docker volume propagation](https://docs.docker.com/storage/bind-mounts/)! E.G. "/PATH/TO/REPO:rw,slave". Write authorisation is required! 122 | - `name_restic_password_file`: Name of a password file in the path "BackupScript > Config > RepositoryPassword" for encrypting your backup repository. 123 | - `restig_backup_tags`: Tags to be assigned to the snapshots of the backup. Can be used to label snapshots. Tags are searchable. 124 | - `name_restic_filter_file`: Name of a configuration file in the path "BackupScript > Config > ResticFilter" to exclude directories/files from the backup. To set it up, see the [documentation](https://restic.readthedocs.io/en/latest/040_backup.html#excluding-files). 125 | - `restic_options`: Possibility to add further options specific to Restic according to the [documentation](https://restic.readthedocs.io/en/latest/040_backup.html#). Example: "--dry-run" 126 | - `name_docker_config_file`: Name of a configuration file in the path "BackupScript > Config > DockerConfig" to control the handling of running Docker containers during the backup. A sample configuration is provided. 127 | - `reverse_docker_start_sequence`: Option to reverse the order of the Docker containers when starting: Options: "true" "false" 128 | - `schedule_forget`: Schedule for the execution of the "Forget" sub-process. Examples: "never" "always" "weekly: Mon Thu Sat" "monthly: 7 21 28" 129 | - `keep_*_for`: Time periods for keeping snapshots. 130 | - `schedule_prune`: Schedule for the execution of the "Prune" sub-process. Examples: "never" "always" "weekly: Mon Thu Sat" "monthly: 7 21 28" 131 | - `rclone_remote_path`: Name of the configuration and path for the remote system. Example: "RemoteName:/PATH/ON/REMOTE" 132 | - `name_rclone_filter_file`: Name of a configuration file in the path "BackupScript > Config > RcloneFilter" to exclude directories/files from the copy to remote system. To set it up, see the [documentation](https://rclone.org/filtering/). 133 | - `rclone_options`: Possibility to add further options specific to Rclone according to the [documentation](https://rclone.org/docs/). Example: "--dry-run" "--max-delete=50" "--bwlimit 10M" "--log-level DEBUG" 134 | 135 | ### Execution of the script ### 136 | 137 | To execute a backup job, the job must be given the necessary authorisation. To do this, follow the instructions "BackupScript > SetupInstruction > Setup_Job.txt". 138 | The "User Scripts" plugin can be used under Unraid to automate the execution of a backup job. Alternatively, every Linux system offers the option of setting up automation via a cron job. You can find detailed instructions here: [Cron-Jobs](https://www.freecodecamp.org/news/cron-jobs-in-linux/). The backup job is then executed at the specified frequency. 139 | 140 | ### Restore Data ### 141 | 142 | With Restic, you can make the content of a backup repository accessible like a normal file system. This allows you to access individual files or directories within the repository without having to restore the entire backup. This process is known as mounting. You can find out how to mount a repository and restore data in the instructions of "BackupScript > SetupInstuctions > Restore_Data.txt". You can find all the options for restoring a backup in the [documentation](https://restic.readthedocs.io/en/latest/050_restore.html#) from restic. 143 | 144 | 145 | 146 | 147 | ## Final Note 148 | 149 | I hope these scripts help you to simplify your backup process. The intention for creating these scripts and writing this guide came about mainly because it took me a relatively long time to use Restic and Rclone over docker containers. Unfortunately, there were no suitable solutions or instructions for me. Therefore, I hope that this will make the way to your backups a little easier for you. 150 | 151 | If you have any questions, comments or errors, please let me know. 152 | 153 | With this in mind, I wish you happy backups! 154 | 155 | 156 | 157 | ## License 158 | 159 | MIT License 160 | 161 | Copyright (c) 2023 David Krumm 162 | 163 | Permission is hereby granted, free of charge, to any person obtaining a copy 164 | of this software and associated documentation files (the "Software"), to deal 165 | in the Software without restriction, including without limitation the rights 166 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 167 | copies of the Software, and to permit persons to whom the Software is 168 | furnished to do so, subject to the following conditions: 169 | 170 | The above copyright notice and this permission notice shall be included in all 171 | copies or substantial portions of the Software. 172 | 173 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 174 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 175 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 176 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 177 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 178 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 179 | SOFTWARE. 180 | --------------------------------------------------------------------------------