シンプルなバックアップシェルスクリプト

## はじめに

もともとGit管理されているリポジトリがあったわけだが、引継ぎ時点で100以上のunstagedなログやらプログラムのバックアップがあった。これから先もこうしていくと気が重くなる。とりあえず、だれでも使えそうな（Gitとは無関係の）バックアップスクリプトを作ってみた。

のを放置していていたので、とりあえずどっかに貼っとかないと永遠に忘れると思い、メモ掲載。

## 書式

```
書式: backup.sh [オプション....] [対象バックアップファイル]
オプション:
 -q 静止モード。コミットメッセージは quiet modeと書かれる
 -d デバッグモード。処理内容を確認できる。
 -l  履歴を見る
 -b  自動モード。何も出力せず、コミットメッセージにはバックアップ日時が書かれる。
 -p [mesg] -bと併用の際、日時の手前に書く
```

### ユースケース

- `crontab` に `backup.sh -b -p 'auto' path/to/file.cpp` とか書いて自動バックアップ
- 重要なバックアップはメッセージ書く

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/209562/7b382f61-9cdb-98d6-2b47-3912f2d0c534.png)

履歴をみたいときは、 `-l` する

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/209562/bbc03ca4-0ca5-702c-6e26-8b94ae5283a3.png)

直近の履歴と同じハッシュならバックアップしない

```bash
mysrv 11:25:48 > ./backup.sh backup.sh
INFO:: ハッシュ値照合中... [backup.sh.20221122d.bk]
INFO:: バックアップファイルと同じハッシュのファイルなため、バックアップを実施しません。
mysrv 11:26:27 >
```

履歴データは単純CSVなので適当にいじれる。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/209562/8f281a80-3aa6-c36c-cc98-51788b1baf5c.png)


## スクリプト本体

```bash
#!/bin/bash -eu
###############################################
# File based backup
# Author: Anya-tokugawa
cat << COMMENT > /dev/null
機能:
 - 日時+アルファベット順+アルファベット文字数でバックアップ

例:
ver1   hoge.sh.20210103a.bk
ver2   hoge.sh.20210105a.bk
ver3   hoge.sh.20210105b.bk
ver4   hoge.sh.20210105c.bk
...
       hoge.sh.20210105z.bk
       hoge.sh.20210105aa.bk
       hoge.sh.20210105ab.bk
...
       hoge.sh.20210105at.bk
       hoge.sh.20210106a.bk
...
latest hoge.sh

 - 一日最大,　a-zzzzまで可能。1ファイル475254回可能。一秒間に５回バックアップ可能。


TODO:
 - バックアップデータに関するハッシュリポジトリを作る。
 - バックアップデータ自体はリポジトリに格納しリンクだけする？
 - 同じハッシュのファイルを集約
 - コミットメッセージの実装（済）
   - 削除されたバックアップファイルの自動検出
   - 複数行のコミットメッセージをつくる。
   - backupDirにしてHashのファイルを作る。
 - 直前とのdiff/colordiffの実装

COMMENT

INFO=1
AUTO=0
DEBUG=0
VIEW=0
mesgPrefix=""
##############################
# リポジトリ設定
INFOFILE=".backup.csv"
if [[ ! -f "$INFOFILE" ]];then
  echo -n '' > "$INFOFILE"
fi
##############################
_LOG(){
 if [[ "$1" == "ERROR" ]];then
   shift
   if [[ $INFO -ne 0 ]];then
    echo "ERROR:: $*"
   fi
   exit 1
 fi
 if [[ "$1" == "INFO" ]] && [[ $INFO -eq 1 ]];then
   shift
   echo "INFO:: $*"
 elif [[ "$1" == "DEBUG" ]] && [[ $DEBUG -eq 1 ]];then
   shift
   echo "DEBUG:: $*"
fi
}

# 19990101a方式のバージョンソート

_sort(){
while read -r line
do
  echo "${#line} $line"
done < /dev/stdin  | sort -n | cut -d " " -f 2-
}


# バックアップ処理
_backup(){

  if [[ $INFO -eq 1 ]] && [[ $AUTO -eq 0 ]];then
    rsync -z --progress "$1" "$2"
  else
    rsync -qz --progress "$1" "$2"
  fi

  status_code=$?

  if [[ $status_code -eq 0 ]];then
    _LOG DEBUG 書き込み禁止化
    chmod a-w "$2"

    mesg=""
    if [[ $INFO -eq 1 ]] && [[ $AUTO -eq 0 ]];then
      echo -n "メッセージ: "
      read -r mesg
    elif [[ $AUTO -eq 1 ]];then
      if [[ "$mesgPrefix" == "" ]];then
        mesg="$(date)"
      else
        mesg="$mesgPrefix $(date)"
      fi
    else
      mesg="quit-mode"
    fi

    chmod +w $INFOFILE

    # baseFileName, VersionStr,HASH, mesg
    echo "${1},${3},${4},${mesg}" >> "$INFOFILE"

    chmod -w $INFOFILE

  fi
}

_usage(){
  echo "書式: $0 [オプション....] [対象バックアップファイル]"
  echo "オプション:"
  echo " -q 静止モード。コミットメッセージは quiet modeと書かれる"
  echo " -d デバッグモード。処理内容を確認できる。"
  echo " -l  履歴を見る"
  echo " -b  自動モード。何も出力せず、コミットメッセージにはバックアップ日時が書かれる。"
  echo " -p [mesg] -bと併用の際、日時の手前に書く。"
}



if [[ $# -lt 1 ]];then
 _usage
 exit 1
fi

NOW="$1"
if [[ $# -gt 1 ]];
then
  while [[ $# -ne 0 ]];
  do
    NOW="$1"
    case "$NOW" in
      "-d") DEBUG=1;;
      "-l") VIEW=1;;
      "-q") INFO=0;;
      "-b") AUTO=1; INFO=0; DEBUG=0;;
      "-p") mesgPrefix="$2";;
    esac
    shift
  done
fi
BackupFile="$NOW"

_LOG DEBUG ファイルの存在チェック
if [[ ! -f "${BackupFile}" ]];then
  _LOG ERROR 対象バックアップファイルが存在しません。
fi
_LOG DEBUG バックアップファイルか
if [[ "${BackupFile}" =~ .*\.bk ]];then
  _LOG ERROR バックアップファイルはバックアップできません。
fi



if [[ $VIEW -eq 1 ]];then
  HEADER="バックアップファイル,バージョン,SHA1SUMハッシュ値,メッセージ"
  cat <(echo "$HEADER") <(grep "^${BackupFile}," $INFOFILE | tail -n $(( $(tput lines) -5 )) ) | column -ts,
  exit 0
fi



_LOG DEBUG 最終バージョンファイルのチェック
LatestVersionFile="$( /bin/ls -1  | grep "^${BackupFile}" | grep -P '\.[0-9][0-9][0-9][0-9][0|1][0-9][1|2|3][0-9][a-z]*?\.bk$' | _sort | tail -1)"
_LOG DEBUG "最終バージョンファイル: $LatestVersionFile"

_LOG DEBUG 最終バージョンのチェック
LatestVersion="$(echo "$LatestVersionFile" | grep -oP '[0-9][0-9][0-9][0-9][0|1][0-9][1|2|3][0-9][a-z]*?\.bk$' |sed 's;\.bk;;g')"
_LOG DEBUG "最終バージョン: $LatestVersion"

_LOG DEBUG 最終バージョン日時のチェック
LatestVersionDate="$(echo "$LatestVersion" | cut -c 1-8 )"
_LOG DEBUG "最終バージョン日時: $LatestVersionDate"

_LOG DEBUG 最終バージョン文字のチェック
LatestVersionChar="$(echo "$LatestVersion" | cut -c 9-  )"
_LOG DEBUG "最終バージョン文字: $LatestVersionChar"

_LOG DEBUG 最終バージョン文字の文字数のチェック
LatestVersionCharNum="$(echo -n "$LatestVersionChar" | wc -c )"
_LOG DEBUG "最終バージョン文字の文字数: $LatestVersionCharNum"

_LOG DEBUG 本日日時のチェック
Today=$(date +'%Y%m%d')
_LOG DEBUG "日付: $Today"

if [[ "$LatestVersionFile" == "" ]];then
  _LOG INFO バックアップファイルが存在しませんでした。初期バックアップを実施します。
  bk="${BackupFile}.${Today}a.bk"
  _backup "${BackupFile}" "$bk" "${Today}a" "$(sha1sum "$BackupFile" | awk '{print $1}' )"
  exit 0
fi


_LOG INFO "ハッシュ値照合中... [$LatestVersionFile]"
bkHash="$(sha1sum "$LatestVersionFile" | awk '{print $1}')"
nowHash="$(sha1sum "$BackupFile" | awk '{print $1}' )"
_LOG DEBUG "最終バックアップファイルのハッシュ値: $bkHash"
_LOG DEBUG "対象バックアップファイルのハッシュ値: $nowHash"

_LOG DEBUG ハッシュ比較
if [[ "$bkHash" == "$nowHash" ]];then
  _LOG INFO バックアップファイルと同じハッシュのファイルなため、バックアップを実施しません。
  exit 1
fi

_LOG DEBUG バックアップ日時を比較
if [[ "$Today" != "$LatestVersionDate" ]];then
  _LOG INFO 本日最初のバックアップを実施します。
  bk="${BackupFile}.${Today}a.bk"
  _backup "${BackupFile}" "$bk" "${Today}a" "$nowHash"
  exit 0
fi

_LOG DEBUG 最終バージョン文字の数よりバックアップ文字列を生成します。
CharVers=()
case ${LatestVersionCharNum} in
  "1") CharVers=( $(echo {a..z}) );;
  "2") CharVers=( $(echo {a..z}{a..z}) );;
  "3") CharVers=( $(echo {a..z}{a..z}{a..z} ) );;
  "4") CharVers=( $(echo {a..z}{a..z}{a..z}{a..z}) );;
  *) _LOG ERROR "Error: NotSupportSubVersion: $LatestVersionChar"
esac

_LOG DEBUG 最終バージョン文字がその文字数における最後のバージョン文字かチェックします。
FirstFlag=0
case ${LatestVersionChar} in
  "z") CharVers=( $(echo {a..z}{a..z}) ); FirstFlag=1;;
  "zz") CharVers=( $(echo {a..z}{a..z}{a..z} ) ); FirstFlag=1;;
  "zzz") CharVers=( $(echo {a..z}{a..z}{a..z}{a..z}) ); FirstFlag=1;;
esac

nextflag=0
NewVersionChar=""


if [[ $FirstFlag -eq 1 ]];then
  _LOG DEBUG 最終バージョン文字がその文字数における最後のバージョン文字列だったため、文字数を拡張しました。
  NewVersionChar="${CharVers[0]}"
else
  _LOG DEBUG 最終バージョン文字がその文字数における最後のバージョン文字列ではないため、次の文字列を検索します。
  for s in "${CharVers[@]}"
  do
    if [[ ${nextflag} -eq 1 ]];then
      _LOG DEBUG "次のバージョン文字列を取得しました。[$s]"
      NewVersionChar="$s"
      break
    elif [[ "$s" == "$LatestVersionChar" ]];then
      _LOG DEBUG "現在のバージョン文字列と一致するものがありました。[$s]"
      nextflag=1
      continue
    fi
  done
fi

_LOG DEBUG 次のバージョン文字列が取得できたか
if [[ "$NewVersionChar" != "" ]];then
  _LOG INFO 継続バックアップを開始します。
  bk="${BackupFile}.${LatestVersionDate}${NewVersionChar}.bk"
  _backup "${BackupFile}" "$bk" "${LatestVersionDate}${NewVersionChar}" "$nowHash"
fi

```