Advertisement
Guest User

Untitled

a guest
Dec 26th, 2014
184
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
Bash 9.29 KB | None | 0 0
  1. #!/usr/bin/env bash
  2.  
  3. APPNAME=$(basename $0 | sed "s/\.sh$//")
  4.  
  5. # -----------------------------------------------------------------------------
  6. # Log functions
  7. # -----------------------------------------------------------------------------
  8.  
  9. fn_log_info()  { echo "$APPNAME: $1"; }
  10. fn_log_warn()  { echo "$APPNAME: [WARNING] $1" 1>&2; }
  11. fn_log_error() { echo "$APPNAME: [ERROR] $1" 1>&2; }
  12.  
  13. # -----------------------------------------------------------------------------
  14. # Make sure everything really stops when CTRL+C is pressed
  15. # -----------------------------------------------------------------------------
  16.  
  17. fn_terminate_script() {
  18.     fn_log_info "SIGINT caught."
  19.     exit 1
  20. }
  21.  
  22. trap 'fn_terminate_script' SIGINT
  23.  
  24. # -----------------------------------------------------------------------------
  25. # Small utility functions for reducing code duplication
  26. # -----------------------------------------------------------------------------
  27.  
  28. fn_parse_date() {
  29.     # Converts YYYY-MM-DD-HHMMSS to YYYY-MM-DD HH:MM:SS and then to Unix Epoch.
  30.     case "$OSTYPE" in
  31.         linux*) date -d "${1:0:10} ${1:11:2}:${1:13:2}:${1:15:2}" +%s ;;
  32.         cygwin*) date -d "${1:0:10} ${1:11:2}:${1:13:2}:${1:15:2}" +%s ;;
  33.         darwin*) date -j -f "%Y-%m-%d-%H%M%S" "$1" "+%s" ;;
  34.     esac
  35. }
  36.  
  37. fn_find_backups() {
  38.     find "$DEST_FOLDER" -type d -name "????-??-??-??????" -prune | sort -r
  39. }
  40.  
  41. fn_expire_backup() {
  42.     # Double-check that we're on a backup destination to be completely
  43.     # sure we're deleting the right folder
  44.     if [ -z "$(fn_find_backup_marker "$(dirname -- "$1")")" ]; then
  45.         fn_log_error "$1 is not on a backup destination - aborting."
  46.         exit 1
  47.     fi
  48.  
  49.     fn_log_info "Expiring $1"
  50.     rm -rf -- "$1"
  51. }
  52.  
  53. # -----------------------------------------------------------------------------
  54. # Source and destination information
  55. # -----------------------------------------------------------------------------
  56.  
  57. SRC_FOLDER="${1%/}"
  58. DEST_FOLDER="${2%/}"
  59. EXCLUSION_FILE="$3"
  60.  
  61. for ARG in "$SRC_FOLDER" "$DEST_FOLDER" "$EXCLUSION_FILE"; do
  62. if [[ "$ARG" == *"'"* ]]; then
  63.         fn_log_error 'Arguments may not have any single quote characters.'
  64.         exit 1
  65.     fi
  66. done
  67.  
  68. # -----------------------------------------------------------------------------
  69. # Check that the destination drive is a backup drive
  70. # -----------------------------------------------------------------------------
  71.  
  72. # TODO: check that the destination supports hard links
  73.  
  74. fn_backup_marker_path() { echo "$1/backup.marker"; }
  75. fn_find_backup_marker() { find "$(fn_backup_marker_path "$1")" 2>/dev/null; }
  76.  
  77. if [ -z "$(fn_find_backup_marker "$DEST_FOLDER")" ]; then
  78.     fn_log_info "Safety check failed - the destination does not appear to be a backup folder or drive (marker file not found)."
  79.     fn_log_info "If it is indeed a backup folder, you may add the marker file by running the following command:"
  80.     fn_log_info ""
  81.     fn_log_info "mkdir -p -- \"$DEST_FOLDER\" ; touch \"$(fn_backup_marker_path "$DEST_FOLDER")\""
  82.     fn_log_info ""
  83.     exit 1
  84. fi
  85.  
  86. # -----------------------------------------------------------------------------
  87. # Setup additional variables
  88. # -----------------------------------------------------------------------------
  89.  
  90. # Date logic
  91. NOW=$(date +"%Y-%m-%d-%H%M%S")
  92. EPOCH=$(date "+%s")
  93. KEEP_ALL_DATE=$(($EPOCH - 86400))       # 1 day ago
  94. KEEP_DAILIES_DATE=$(($EPOCH - 2678400)) # 31 days ago
  95.  
  96. export IFS=$'\n' # Better for handling spaces in filenames.
  97. PROFILE_FOLDER="$HOME/.$APPNAME"
  98. DEST="$DEST_FOLDER/$NOW"
  99. PREVIOUS_DEST="$(fn_find_backups | head -n 1)"
  100. INPROGRESS_FILE="$DEST_FOLDER/backup.inprogress"
  101.  
  102. # -----------------------------------------------------------------------------
  103. # Create profile folder if it doesn't exist
  104. # -----------------------------------------------------------------------------
  105.  
  106. if [ ! -d "$PROFILE_FOLDER" ]; then
  107.     fn_log_info "Creating profile folder in '$PROFILE_FOLDER'..."
  108.     mkdir -- "$PROFILE_FOLDER"
  109. fi
  110.  
  111. # -----------------------------------------------------------------------------
  112. # Handle case where a previous backup failed or was interrupted.
  113. # -----------------------------------------------------------------------------
  114.  
  115. if [ -f "$INPROGRESS_FILE" ]; then
  116.     if [ -n "$PREVIOUS_DEST" ]; then
  117.         # - Last backup is moved to current backup folder so that it can be resumed.
  118.         # - 2nd to last backup becomes last backup.
  119.         fn_log_info "$INPROGRESS_FILE already exists - the previous backup failed or was interrupted. Backup will resume from there."
  120.         mv -- "$PREVIOUS_DEST" "$DEST"
  121.         if [ "$(fn_find_backups | wc -l)" -gt 1 ]; then
  122.             PREVIOUS_DEST="$(fn_find_backups | sed -n '2p')"
  123.         else
  124.             PREVIOUS_DEST=""
  125.         fi
  126.     fi
  127. fi
  128.  
  129. # Run in a loop to handle the "No space left on device" logic.
  130. while : ; do
  131.  
  132.     # -----------------------------------------------------------------------------
  133.     # Check if we are doing an incremental backup (if previous backup exists).
  134.     # -----------------------------------------------------------------------------
  135.  
  136.     LINK_DEST_OPTION=""
  137.     if [ -z "$PREVIOUS_DEST" ]; then
  138.         fn_log_info "No previous backup - creating new one."
  139.     else
  140.         # If the path is relative, it needs to be relative to the destination. To keep
  141.         # it simple, just use an absolute path. See http://serverfault.com/a/210058/118679
  142.         PREVIOUS_DEST="$(cd "$PREVIOUS_DEST"; pwd)"
  143.         fn_log_info "Previous backup found - doing incremental backup from $PREVIOUS_DEST"
  144.         LINK_DEST_OPTION="--link-dest='$PREVIOUS_DEST'"
  145.     fi
  146.  
  147.     # -----------------------------------------------------------------------------
  148.     # Create destination folder if it doesn't already exists
  149.     # -----------------------------------------------------------------------------
  150.  
  151.     if [ ! -d "$DEST" ]; then
  152.         fn_log_info "Creating destination $DEST"
  153.         mkdir -p -- "$DEST"
  154.     fi
  155.  
  156.     # -----------------------------------------------------------------------------
  157.     # Purge certain old backups before beginning new backup.
  158.     # -----------------------------------------------------------------------------
  159.  
  160.     # Default value for $PREV ensures that the most recent backup is never deleted.
  161.     PREV="0000-00-00-000000"
  162.     for FILENAME in $(fn_find_backups | sort -r); do
  163.         BACKUP_DATE=$(basename "$FILENAME")
  164.         TIMESTAMP=$(fn_parse_date $BACKUP_DATE)
  165.  
  166.         # Skip if failed to parse date...
  167.         if [ -z "$TIMESTAMP" ]; then
  168.             fn_log_warn "Could not parse date: $FILENAME"
  169.             continue
  170.         fi
  171.  
  172.         if   [ $TIMESTAMP -ge $KEEP_ALL_DATE ]; then
  173.             true
  174.         elif [ $TIMESTAMP -ge $KEEP_DAILIES_DATE ]; then
  175.             # Delete all but the most recent of each day.
  176.             [ "${BACKUP_DATE:0:10}" == "${PREV:0:10}" ] && fn_expire_backup "$FILENAME"
  177.         else
  178.             # Delete all but the most recent of each month.
  179.             [ "${BACKUP_DATE:0:7}" == "${PREV:0:7}" ] && fn_expire_backup "$FILENAME"
  180.         fi
  181.  
  182.         PREV=$BACKUP_DATE
  183.     done
  184.  
  185.     # -----------------------------------------------------------------------------
  186.     # Start backup
  187.     # -----------------------------------------------------------------------------
  188.  
  189.     LOG_FILE="$PROFILE_FOLDER/$(date +"%Y-%m-%d-%H%M%S").log"
  190.  
  191.     fn_log_info "Starting backup..."
  192.     fn_log_info "From: $SRC_FOLDER"
  193.     fn_log_info "To:   $DEST"
  194.  
  195.     CMD="rsync"
  196.     CMD="$CMD --compress"
  197.     CMD="$CMD --numeric-ids"
  198.     CMD="$CMD --links"
  199.     CMD="$CMD --hard-links"
  200.     CMD="$CMD --one-file-system"
  201.     CMD="$CMD --archive"
  202.     CMD="$CMD --itemize-changes"
  203.     CMD="$CMD --verbose"
  204.     CMD="$CMD --human-readable"
  205.     CMD="$CMD --log-file '$LOG_FILE'"
  206.     if [ -n "$EXCLUSION_FILE" ]; then
  207.         # We've already checked that $EXCLUSION_FILE doesn't contain a single quote
  208.         CMD="$CMD --exclude-from '$EXCLUSION_FILE'"
  209.     fi
  210.     CMD="$CMD $LINK_DEST_OPTION"
  211.     CMD="$CMD -- '$SRC_FOLDER/' '$DEST/'"
  212.     CMD="$CMD | grep -E '^deleting|[^/]$'"
  213.  
  214.     fn_log_info "Running command:"
  215.     fn_log_info "$CMD"
  216.  
  217.     touch -- "$INPROGRESS_FILE"
  218.     eval $CMD
  219.  
  220.     # -----------------------------------------------------------------------------
  221.     # Check if we ran out of space
  222.     # -----------------------------------------------------------------------------
  223.  
  224.     # TODO: find better way to check for out of space condition without parsing log.
  225.     NO_SPACE_LEFT="$(grep "No space left on device (28)\|Result too large (34)" "$LOG_FILE")"
  226.  
  227.     if [ -n "$NO_SPACE_LEFT" ]; then
  228.         fn_log_warn "No space left on device - removing oldest backup and resuming."
  229.  
  230.         if [[ "$(fn_find_backups | wc -l)" -lt "2" ]]; then
  231.             fn_log_error "No space left on device, and no old backup to delete."
  232.             exit 1
  233.         fi
  234.  
  235.         fn_expire_backup "$(fn_find_backups | tail -n 1)"
  236.  
  237.         # Resume backup
  238.         continue
  239.     fi
  240.  
  241.     # -----------------------------------------------------------------------------
  242.     # Check whether rsync reported any errors
  243.     # -----------------------------------------------------------------------------
  244.    
  245.     if [ -n "$(grep "rsync:" "$LOG_FILE")" ]; then
  246.         fn_log_warn "Rsync reported a warning, please check '$LOG_FILE' for more details."
  247.     fi
  248.     if [ -n "$(grep "rsync error:" "$LOG_FILE")" ]; then
  249.         fn_log_error "Rsync reported an error, please check '$LOG_FILE' for more details."
  250.         exit 1
  251.     fi
  252.  
  253.     # -----------------------------------------------------------------------------
  254.     # Add symlink to last successful backup
  255.     # -----------------------------------------------------------------------------
  256.  
  257.     rm -rf -- "$DEST_FOLDER/latest"
  258.     ln -vs -- "$(basename -- "$DEST")" "$DEST_FOLDER/latest"
  259.  
  260.     rm -f -- "$INPROGRESS_FILE"
  261.     rm -f -- "$LOG_FILE"
  262.    
  263.     fn_log_info "Backup completed without errors."
  264.  
  265.     exit 0
  266. done
Advertisement
Add Comment
Please, Sign In to add comment
Advertisement