Pull to refresh

Bash скрипт для создания архива данных

Reading time12 min
Views19K
На днях озадачился резервным копированием данных в облако. Нашёл подходящий сервис попробовал, и понял, что существует необходимость в сжатии бэкапа перед отправкой (думаю нет необходимости объяснять зачем). Не стал заморачиваться в поиске готовых решений и решил сам написать скромный скриптик для этой цели. Исходные файл или папка жмутся в .tar.xz с уровнем сжатия 9, что позволяет сохранить права и выдаёт хорошую компрессию на выходе (у меня снэпшот системы сжимается 4 раза). Результатом остался доволен, думаю для малого бизнеса, да и для личных целей многим пригодиться.

Возможности скрипта:
  • гибкая настройка
  • проверка на доступность ресурсов (источник, директория назначения, рабочая директория)
  • проверка на файл блокировки (предотвращает выполнение если источник еще создаётся)
  • вывод информации о сжатии (размер источника, размер архива, соотношение этих размеров)
  • логирование и дебагинг (вывод дополнительной информации о процессе выполнения)
  • возможность менять вывод (как в консоль и лог-файл, так и только в лог-файл)
  • сохраняет и ротирует предыдущие архивы
  • возможность форматирования текста вывода
  • отправка e-mail-а в случае успешного и/или неуспешного завершения

Собственно сам скрипт (версия 3.0)
#!/bin/bash
################################################################################
##    Bash script for file or directory compression                           ##
################################################################################
## 1. Configuration variables
##  $srcName - Source, file or directory to compress.
##  $srcDir - Directory where the source resides.
##  $runDir - Runtime directory. Lock and log files reside here.
##  $outDir - Directory where the archive will be placed.
##  $archDir - Directory where the previous archives will be placed.
##  $archName - Archive file name.
##    [ example: "archive_$(date +"[%Y.%m.%d - %H:%M:%S]")" ]
##  $logName - Log file name.
##  $lockCheck - Check for lock file or not.
##    If you have condition(s) in which creation of an archive is not
##    recommended or fatal (e.g. while creating snapshot, directory must not
##    be tempered with in any way) and the process which is running prior to
##    archivation (rsnapshot, rsync, rdiff etc.), can create a lock file,
##    you must switch this check ON and fill out a name for a lock file,
##    to prevent integrity violation.
##    [ default: true ]
##  $lockName - Lock file name.
##  $lockSleep - How much time to wait until next try (in milliseconds).
##    [ default: 600 ] - 10 minutes.
##  $lockWait - How many tries before script stop.
##    [ default: 12 ] - Retry for 2 hours if $lockSleep = 600
##  oldArchDaysBack - How much time (in days) into the past, search must look
##    for old archive files inside the vault ($archDir) for deleting the oldest.
##    [ default: 30 ] - One month.
##    @WARNING! If you set $maxOldArch to say 10 and you run archivation process
##      say every 24 hours, but your $oldArchDaysBack is set to 7, script will
##      not be able to account for 3 oldest archive files, so your vault can
##      grow more than 10!
##  maxOldArch - How much old archive files must be kept inside the vault.
##    [ default: 30 ]
##    @WARNING! Do not set higher than $oldArchDaysBack if you run this script
##      daily!
##  $mailSendSucc - Enable mail sending on success.
##  $mailSendFail - Enable mail sending on fail.
##  $mailFileName - Mail file name.
##  $mailTo - To which e-mail messages must be sent.
##  $mailSubject - Subject of the message.
##  $stdType - Sets output to console and or log file.
##    [ default: "cl" ] { "cl" = console & log, "l" = log only }
##  $txtFormat - Allow text formating (bold and colored text).
##    [ default: true ]
##  $debug - Enable debugging or not.
##    [ default: true ]
##  $timeStamp - Timestamp format.
##    [ default: "[%Y.%m.%d - %H:%M:%S]" ]
##  $compressor - Compressor and it's params.
##    [ default: "xz -T 4 -9 -c" ]
##  $archExt - Archive extension, depending on $compressor setting.
##    [ default: ".tar.xz" ]
##
## 2. Helper functions
##  msg() - Prints messages.
##    [ example: msg "Archive file name is: %s\n" "$arcname" ]
##  tf() - Formats text.
##  tStamp() - Prints timestamp.
##    [ example: "$(tStamp "[%Y.%m.%d - %H:%M:%S]")" ]
##    [ fallback: "${timeStamp}" ]
##  secToTime() - Turns seconds into readable time (e.g. 3h 45m 21s).
##  sendMail() - Sends e-mails.
##  ifDebug() - Checks if debuging is enabled.
##  ifLock() - Checks if lock file check is enabled.
##  succ() - Process success output.
##  fail() - Process fail output.
## 3. Functions
##  checkConf() - Validates configuration.
##  checkLock() - Checks for lock file.
##  checkForOldArch() - Checks old archive files.
##  execComp() - Executes compression and assigns execution time to $execTime.
##  size() - Gets object size values.
##    @Takes two parameters, object and presentation format
##      (h - human readable, b - bytes)!
##    [ example: "$(size "$src" "b")" ]
##  compRatio() - Gets compression ratio.
##    @Takes two parameters, source and archive!
##    [ example: "$(compRatio "$src" "$archive")" ]
##  compInf() - Outputs compression info.
##    [ example: compInf $src $archive ]
################################################################################

## =========================== ##
## Set configuration variables ##
## =========================== ##

srcName="test-file"
srcDir="/tmp/"
runDir="/tmp/run/"
outDir="/tmp/out/"
archDir="/tmp/vault/"
archName="arch"
logName=$archName
lockCheck=true
lockName=$archName
lockSleep=600
lockWait=12
oldArchDaysBack=30
maxOldArch=10
mailSendSucc=false
mailSendFail=true
mailFileName=$archName
mailTo="your@email.address"
mailSubjectSucc="$0 says: Process has finished successfuly!"
mailSubjectFail="$0 says: Warning! Process has failed!"
stdType="cl"
txtFormat=true
debug=true
## Do not change this variables if you don't know how!
timeStamp="[%Y.%m.%d - %T]"
compressor="xz -T 4 -9 -c"
archExt=".tar.xz"

## ======================================= ##
## WARNING! Do not modify past this point! ##
## ======================================= ##

## ---------------- ##
## Global variables ##
## ---------------- ##

export TERM=xterm
TIMEFORMAT="%E"

runDir=${runDir%/} ; srcDir=${srcDir%/} ; srcName=${srcName%/} ## Deslashify.
outDir=${outDir%/} ; archDir=${archDir%/} ## Deslashify.
src="$srcDir/$srcName" ## Full path to source.
archiveFN="$archName$archExt" ## Archive file name and extension.
archive="$outDir/$archiveFN" ## Full path to archive.
logFile="$runDir/$logName.log" ## Full path to log file.
logFileF="$runDir/$logName.f.log" ## Full path to log file.
lockFile="$runDir/$lockName.lock" ## Full path to lock file.
mailFile="$runDir/$mailFileName.mail" ## Full path to mail file.
execute=0 ## Allow execution.
execTime="" ## Compression execution time.

## ------------- ##
## Initial setup ##
## ------------- ##

exec 3>&1 1>>$logFile 2>&1 ## Modifies std output.

## ---------------- ##
## Helper functions ##
## ---------------- ##

## Prints messages.
msg() { [[ $stdType == "cl" ]] && printf "$@" | tee /dev/fd/3 || printf "$@" ; }

## Text formating.
tf() {
  if [[ $txtFormat == true ]] ; then
    res=""
    for ((i=2; i<=$#; i++)) ; do
      case "${!i}" in
        "bold" ) res="$res\e[1m" ;;
        "underline" ) res="$res\e[4m" ;;
        "reverse" ) res="$res\e[7m" ;;
        "red" ) res="$res\e[91m" ;;
        "green" ) res="$res\e[92m" ;;
        "yellow" ) res="$res\e[93m" ;;
      esac
    done
    echo -e "$res$1\e[0m"
  else
    echo "$1"
  fi
}

## Sets timestamp.
tStamp() {
  if [[ -n ${1} ]] && [[ ! -n ${2} ]] ; then
    date +"${1}"
  elif [[ -n ${1} ]] && [[ -n ${2} ]] ; then
    date -d "${2}" +"${1}"
  else
    date +"${timeStamp}"
  fi
}

## Seconds to readable time.
secToTime() {
  timeInSec=$1
  if [[ $timeInSec -ge 0 ]] && [[ $timeInSec -le 59 ]]; then
    echo "${timeInSec}s"
  elif [[ $timeInSec -ge 60 ]] && [[ $timeInSec -le 3599 ]]; then
    m=$(( timeInSec / 60 ))
    s=$(( timeInSec % 60 ))
    echo "${m}m ${s}s"
  elif [[ $timeInSec -ge 3600 ]] && [[ $timeInSec -le 86399 ]]; then
    h=$(( timeInSec / 3600 ))
    m=$(( (timeInSec % 3600) / 60 ))
    s=$(( (timeInSec % 3600) % 60 ))
    echo "${h}h ${m}m ${s}s"
  fi
}

## Sends e-mail.
sendMail() {
  if [[ ! -n $1 ]] && [[ $mailSendSucc == true ]] ; then
    echo "Process finished, no errors found!" > $mailFile
    mail -s "$mailSubjectSucc" $mailTo < $mailFile
  fi
  if [[ -n $1 ]] && [[ $mailSendFail == true ]] ; then
    echo "Process has failed at step: $1" > $mailFile
    mail -s "$mailSubjectFail" $mailTo < $mailFile
  fi
}

## Checks debug status.
ifDebug() { [[ $debug == true ]] ; }

## Checks lock status.
ifLock() { [[ $lockCheck == true ]] ; }

## Outputs process success.
succ() { msg "%s %s!\n\n" "$(tStamp)" "$(tf "SUCCESS" "bold" "green")" \
 ; sendMail ; exit 0 ; }

## Outputs process fail.
fail() { msg "%s %s at %s!\n\n" "$(tStamp)" "$(tf "FAIL" "bold" "red")" \
 "$(tf "$1" "bold" "underline" "yellow")" ; sendMail "$1" ; exit 1 ; }

## --------- ##
## Functions ##
## --------- ##

## Checks configuration.
checkConf() {

  ## Check file/directory permissions.
  checkPerm() {
    if [[ ! -n ${2} ]] ; then
      [[ -r ${1} ]] && [[ -w ${1} ]] && echo 1 || echo 0 ;
    else
      case "$2" in
        "f" ) [[ -f ${1} ]] && [[ -r ${1} ]] && [[ -w ${1} ]] && \
         echo 1 || echo 0 ;;
        "d" ) [[ -d ${1} ]] && [[ -r ${1} ]] && [[ -w ${1} ]] && \
         echo 1 || echo 0 ;;
        "fd" ) [[ -d ${1} ]] || [[ -f ${1} ]] && [[ -r ${1} ]] && \
         [[ -w ${1} ]] && echo 1 || echo 0 ;;
      esac
    fi
  }

  ## Output file/directory status.
  status() { [[ $1 == 1 ]] && tf "OK" "bold" "green" || \
   tf "NOT OK" "bold" "red" ; }

  ## Check source.
  pass=$(( pass + $(checkPerm "$src" "fd") ))
  ifDebug && msg " -> Source\t\t%s\tis set to: %s\n" \
   "$(status "$(checkPerm "$src" "fd")")" "$(tf $src "bold")"

  ## Check runtime directory.
  pass=$(( pass + $(checkPerm "$runDir" "d") ))
  ifDebug && msg " -> Runtime directory   %s\tis set to: %s\n" \
   "$(status "$(checkPerm "$runDir" "d")")" "$(tf $runDir "bold")"

  ## Check output directory.
  pass=$(( pass + $(checkPerm "$outDir" "d") ))
  ifDebug && msg " -> Output  directory   %s\tis set to: %s\n" \
   "$(status "$(checkPerm "$outDir" "d")")" "$(tf $outDir "bold")"

  ## Check archive directory.
  pass=$(( pass + $(checkPerm "$archDir" "d") ))
  ifDebug && msg " -> Archive directory   %s\tis set to: %s\n" \
   "$(status "$(checkPerm "$archDir" "d")")" "$(tf $archDir "bold")"

  ## Display rest of the config.
  ifDebug && msg " -> Archive\t\t\tis set to: %s\n" \
   "$(tf $archiveFN "bold")"
  ifDebug && msg " -> Log file\t\t\tis set to: %s\n" \
   "$(tf $logName "bold")"
  ifDebug && ifLock && msg " -> Lock file\t\t\tis set to: %s\n" \
   "$(tf $lockName "bold")"
  ifDebug && msg " -> Timestamp\t\t\tis set to: %s\n" \
   "$(tf "$timeStamp" "bold")"
  ifDebug && msg " -> Compressor\t\t\tis set to: %s\n" \
   "$(tf "$compressor" "bold")"

  ## Validate config
  if [[ $pass == 4 ]] ; then
    ifDebug && msg " -> Configuration is    %s\n" "$(status 1)"
  else
    msg " -> Configuration is    %s, exiting...\n" "$(status 0)"
    fail "configuration check"
  fi
}

## Checks for lock file.
checkLock() {
  [[ -f ${lockFile} ]] && ifDebug && msg \
   " -> Lock file %s, waiting iterations are set to: %s\n" \
    "$(tf "is in place" "bold")" "$(tf "$lockWait" "bold")"
  for ((i=1; i<=lockWait; i++)) ; do
    if [[ -f ${lockFile} ]] ; then
      ifDebug && msg " -> waiting for %s (%s)\n" \
       "$(tf "$(secToTime "$lockSleep")" "bold")" \
        "$(tf "$i" "bold" "yellow")" ; sleep $lockSleep
    else
      i=1000
    fi
    if [[ $i == "$lockWait" ]] ; then
      msg " -> Lock file %s, exiting...\n" "$(tf "still in place" "bold")"
      fail "lock file check"
    fi
  done
}

## Checks if the old archive exists, if it does, deletes it.
checkForOldArch() {
  if [[ -f ${archive} ]] ; then

    oldArchCount() {
      res=$(printf "%s" "$(ls -afq $archDir | wc -l)")
      echo $(( res - 2 ))
    }
    oldestArch() {
      find $archDir -type f -mtime -$oldArchDaysBack -print0 \
       | xargs -0 ls -tr | head -n 1
    }

    ## Move previous archive to the vault and rename it.
    ifDebug && msg \
     " -> Previous archive file %s, created on %s in %s, moving...\n" \
      "$(tf "exists" "bold" "yellow")" \
       "$(tf "$(date -r $archive +"%Y.%m.%d")" "bold")" \
        "$(tf "$(date -r $archive +"%R")" "bold")"
    prevArch="$archDir/$archName$(date -r $archive +"_%Y%m%d-%H%M%S")$archExt"
    mv -f "$archive" "$prevArch" || fail "moving archive to the vault"

    ## Check if previous archive were moved to the vault.
    if [[ -f ${prevArch} ]] && [[ ! -f ${archive} ]] ; then
      ifDebug && msg \
       " -> Previous archive %s to the vault as: %s, proceeding...\n" \
        "$(tf "moved" "bold" "green")" "$(tf "$prevArch" "bold")"
      execute=$(( execute + 1 ))
    else
      msg " -> %s %s previous archive to the vault, exiting...\n" \
       "$(tf "WARNING!" "bold" "yellow")" "$(tf "Can't move" "bold")"
      fail "moving archive to the vault"
    fi

    ## Count old archive files inside the vault and delete the oldest.
    oldArchCount=$(oldArchCount)
    if [[ $oldArchCount -gt $maxOldArch ]] ; then
      ifDebug && msg " -> Number of old archives inside the vault is: %s\n" \
       "$(tf "$(oldArchCount)" "bold")"
      for ((i=oldArchCount; i>maxOldArch; i--)) ; do
        oldestArch=$(oldestArch)
        rm -f "$oldestArch" || fail "delete oldest archive"
        if [[ ! -f ${oldestArch} ]] ; then
          ifDebug && msg " -> Oldest file (%s) %s, proceeding...\n" \
           "$(tf "$oldestArch" "bold")" "$(tf "was deleted" "bold" "yellow")"
        else
          msg " -> %s %s the oldest archive (%s), exiting...\n" \
           "$(tf "WARNING!" "bold" "yellow")" "$(tf "Can't delete" "bold")" \
            "$(tf "$oldestArch" "bold")" ; fail "delete oldest archive"
        fi
      done
    fi

    ## Check if old archives were indeed deleted.
    ifDebug && msg " -> Number of old archives inside the vault is: %s\n" \
     "$(tf "$(oldArchCount)" "bold")"
    oldArchCount=$(oldArchCount)
    if [[ $oldArchCount -gt 10 ]] ; then
      msg " -> %s %s the oldest archive(s), exiting...\n" \
       "$(tf "WARNING!" "bold" "yellow")" "$(tf "Can't delete" "bold")"
      fail "delete oldest archive(s)"
    else
      execute=$(( execute + 1 ))
    fi

  else
    ifDebug && msg " -> Old archive %s, proceeding...\n" \
     "$(tf "does not exist" "bold" "green")" ; execute=2
  fi
}

## Executes compression.
execComp() {
  ## Go to source directory.
  cd $srcDir || fail "cd to source directory"
  if [[ ${PWD} != "$srcDir" ]] ; then
    msg " -> %s Can't cd to source directory, exiting...\n%s" \
     "$(tf "WARNING!" "bold" "yellow")" ; fail "cd to source directory"
  fi
  execTime=$( { time tar cf - $srcName | $compressor - > $archive ; } 2>&1 )
}

## Gets object size values.
size() {
  [[ $2 == "b" ]] && du -bs "$1" | awk '{ print $1 }'
  [[ $2 == "h" ]] && du -hs "$1" | awk '{ print $1 }'
}

## Rounds floating numbers.
round() { printf %."$2"f "$(echo "(((10^$2)*$1)+0.5)/(10^$2)" | bc)" ; }

## Gets compression ratio.
compRatio() { printf "%.*f" 2 "$(let res="$1/$2"; printf "%s" "$res")" ; }

## Outputs compression info.
compInf() {
  msg "\t-> Source size: %s (%s bytes)\n" \
   "$(tf "$(size "$1" "h")" "bold")" "$(tf "$(size "$1" "b")" "bold")"
  msg "\t-> Archive size: %s (%s bytes)\n" \
   "$(tf "$(size "$2" "h")" "bold")" "$(tf "$(size "$2" "b")" "bold")"
  msg "\t-> Compression ratio: %s\n" \
   "$(tf "$(compRatio "$(size "$1" "b")" "$(size "$2" "b")")" "bold")"
}

## Begins compression process.
compress() {
  if [[ $execute == 2 ]] ; then
    ifDebug && msg " -> Flag %s set, executing...\n" "$(tf "is" "bold" "green")"
    execComp

    execTime=$(round "$execTime" 0)
    execTime=$(secToTime "$execTime")

    ## Check if archive file created.
    if [[ -f ${archive} ]] ; then
      msg " -> Archive %s created in %s!\n" "$(tf $archive "bold")" \
       "$(tf "$execTime" "bold")" ; compInf $src $archive ; succ
    else
      msg " -> %s Archive file was %s created, exiting...\n" \
       "$(tf "WARNING!" "bold" "yellow")" "$(tf "not" "bold" "red")"
      fail "archive file creation"
    fi
  else
    msg " -> Flag %s set, exiting...\n" "$(tf "is not" "bold" "red")"
    fail "execution flag check"
  fi
}

## ---------- ##
## Initialize ##
## ---------- ##
msg "%s Initializing...\n" "$(tStamp)"
checkConf ## Check configuration.
ifLock && checkLock ## Check for lock file.
checkForOldArch ## Check for old archive file.
compress ## Begin compression.


P.S.
Тем, кому пригодился и есть желание доработать или сделать более элегантным, прошу на GitHub.
Tags:
Hubs:
+6
Comments22

Articles