Skip to content

Latest commit

 

History

History
316 lines (288 loc) · 9.68 KB

babel.zh-CN.org

File metadata and controls

316 lines (288 loc) · 9.68 KB

在 GNU Emacs 下,按 C-c C-v t 即可输出(tangle)代码文件。

buttersnap

开头

shebang 与元信息。

#!/bin/bash

# Butter Snap - make periodic snapshots of btrfs filesystem
#
# Copyright (C) 2024 Celestial.y celestial.y@outlook.com
#
# This program is distributed under the GNU General Public License
# http://www.gnu.org/licenses/gpl.txt

设置 shell 属性;不过,都不太合适。

  • -u :若调用了一个不存在的变量,则报错。
  • -e :遇错则退出。
set -ue

一些变量配置。其中 prog 的值是脚本文件名;PATH 的值则是在原有基础上附加了一些变量。

LOG_FACILITY=local0
VERSION="0.1.0"
prog=${0##*/}
PATH="$PATH:/usr/sbin:/usr/bin:/sbin:/bin"

定义函数

function join_by { local IFS="$1"; shift; echo "$*"; }

function log.info() {
    logger -p ${LOG_FACILITY}.info -t ${prog} "$1"
    if test -z "$quiet"
    then
        echo "$1"
    fi
}

function log.error() {
    logger -p ${LOG_FACILITY}.err -t ${prog} "$1"
    echo "ERROR: $1" >&2
}

function show_help() {
echo \
"Syntax: ${prog} <target> [options]
${prog} -V for version.
See https://github.com/clsty/butter-snap for details."
}

function show_version() {
echo "${prog} Version ${VERSION}"
}

function show_all_btrfs() {
declare -a mountpoints=()
declare -a targets=()
for i in $(cat /proc/mounts | grep '[[:space:]]btrfs[[:space:]]' | tr -s ' ' | cut -f 2 -d ' '); do
  local id=$(btrfs subvolume show $i | grep '^[[:blank:]]*Subvolume ID:' | awk '{ print $3 }')
  local targets+=("$i")
  for j in $(btrfs subvolume list $i | grep " top level $id " | awk '{ print $9 }');do
    local targets+=("$i$j")
  done
done
for i in "${targets[@]}"; do echo "$i";done
}

处理参数

初始化变量

store_path=""
readonly=false
quiet=""
use_transid=false
omit_error_code=0
time_duration=0
snapname_ops=""

定义选项(供 getopt 处理)

# Should be processed first
ops_o+=(h)  ops_l+=(help)
ops_o+=(V)  ops_l+=(version)
            ops_l+=(show-all-btrfs)
# Should be processed later
ops_o+=(E)  ops_l+=(no-omit)
ops_o+=(k:)  ops_l+=(keep:)
ops_o+=(q)  ops_l+=(quiet)
ops_o+=(r)  ops_l+=(readonly)
ops_o+=(s:) ops_l+=(store-dir:)
ops_o+=(S:) ops_l+=(store-pathtype:)
ops_o+=(t:) ops_l+=(time:)
ops_o+=(T)  ops_l+=(use-transid)
# About snapname
ops_o+=(n:) ops_l+=(snapname-adj:)
ops_o+=(N:) ops_l+=(snapname-type:)
ops_o+=(o:) ops_l+=(snapname-ops:)
            ops_l+=(snapname-value:)
            ops_l+=(snapname-pattern:)

用 getopt 处理参数

para=$(getopt -o $(join_by , "${ops_o[@]}") \
              -l $(join_by , "${ops_l[@]}") \
              -n "$0" -- "$@")
[ $? != 0 ] && { log.error "Failed processing getopt, please recheck parameters."; exit 1; }

处理各选项

eval set -- "$para"
while true;do case "$1" in
  -h|--help)             show_help ;exit 0;;
  -V|--version)          show_version ;exit 0;;
     --show-all-btrfs)   show_all_btrfs ;exit 0;;

  -E|--no-omit)          omit_error_code=1 ;shift;;
  -k|--keep)             keep_num="$2" ;shift 2;;
  -q|--quiet)            quiet="1" ;shift;;
  -r|--readonly)         readonly="true" ;shift;;
  -s|--store-dir)        store_dir=$2 ;shift 2;;
  -S|--store-pathtype)   store_pathtype=$2 ;shift 2;;
  -t|--time)             time_duration=$((0+$2)) ;shift 2;;
  -T|--use-transid)      use_transid=true ;shift ;;

  -n|--snapname-adj)     snapname_adj="$2" ;shift 2;;
  -N|--snapname-type)    snapname_type="$2" ;shift 2;;
  -o|--snapname-ops)     snapname_ops="$2" ;shift 2;;
     --snapname-value)   customed_snapname="$2" ;shift 2;;
     --snapname-pattern) customed_snapname_pattern="$2" ;shift 2;;
  --) shift;break;;
  *) log.error "Unknown argument: $1";show_help ;exit 1;;
esac;done

处理非选项的参数

处理第一个(也是最后一个)非选项参数:挂载点。

  • realpath 化。
  • 检查它是否为 Btrfs 子卷的挂载点或 Btrfs 子卷本身。
# Canonicalize the mountpoint path (strip trailing slashes, etc)
[[ -z "$1" ]] && { show_help; exit; } || target=$(realpath -m $1)
# Verify that the path is either a valid btrfs mountpoint
if findmnt -t btrfs -T "${target}" &> /dev/null; then
  log.info "Target is the mountpoint of a Btrfs (sub)volume: ${target}"
# or a valid snapshot matching target
elif btrfs subvolume show $target > /dev/null; then
  log.info "Target is the path of a Btrfs (sub)volume: ${target}"
else
  log.error "Target must be the path or mountpoint of a Btrfs (sub)volume: ${target}"; exit 1
fi

处理保留数

if [ ! "${keep_num:=5}" -ge 0 ] ; then log.error "Keep number \"$keep_num\" is not a number or is less than 0.";exit 1; fi
keep_num=$(( $keep_num+1 ))

处理 store_path

赋值 $store_path

store_pathtype=${store_pathtype:-rel}
store_dir=${store_dir:-.snapshots}
case ${store_pathtype} in
    rel) store_path="${target}"/"${store_dir}";;
    mim) store_path="${store_dir}"/"${target}";;
    abs) store_path="${store_dir}";;
    *) log.error "False value \"${store_pathtype}\" for store_pathtype. Possible value: rel, mim, abs.";exit 1;;
esac

创建目录 $store_path

if [ ! -d $store_path ]; then
    log.info "Creating $store_path"
    mkdir -p $store_path
fi
store_path=$(readlink -f $store_path)

处理 snapname

先处理 snapname 的附加选项

time_delim=":"
adj_as_prefix=true
for opt in $(echo "$snapname_ops" | tr "," "\n");do case $opt in
  compatible) time_delim="-";;
     postfix) adj_as_prefix=false;;
     *) log.error "Not supported snapname option: \"$opt\"";exit 1 ;;
esac;done

再据 snapname type 来处理

snapname_adj="${snapname_adj:-snapshot}"
case ${snapname_type:=default} in
  default)
    if ${adj_as_prefix}; then
      snapname=${snapname_adj}_$(date +%Y-%m-%d_%H${time_delim}%M${time_delim}%S)
      snapname_pattern="${snapname_adj}_????-??-??_??${time_delim}??${time_delim}??"
    else
      snapname=$(date +%Y-%m-%d_%H${time_delim}%M${time_delim}%S)_${snapname_adj}
      snapname_pattern="????-??-??_??${time_delim}??${time_delim}??_${snapname_adj}"
    fi;; 
  vfs)
    snapname=$(TZ=GMT date +@GMT-%Y.%m.%d-%H.%M.%S)
    snapname_pattern="@GMT-????.??.??-??.??.??"
    ;;
  custom)
    snapname="$customed_snapname"
    snapname_pattern="$customed_snapname_pattern"
    ;;
  *) log.error "Not supported snapname type: \"${snapname_type}\"";exit 1 ;;
esac

检查

if [ -z "${snapname}" ]; then
  log.error "Empty snapname.";exit 1
elif [ -z "${snapname_pattern}" ]; then
  log.error "Empty snapname pattern.";exit 1
elif [ -e "${store_path}/${snapname}" ]; then
  log.error "Snapshot could not be created at \"${store_path}/${snapname}\" because it already exists.";exit 1
fi

检查时间

函数:据 transid 检查是否有变化

checktime_eq_transid(){
  # get transaction ids
  id_snap=$(btrfs subvolume find-new "$newestSnapshot" 99999999| sed 's/[^0-9]//g')
  id_mount=$(btrfs subvolume find-new "${target}" 99999999| sed 's/[^0-9]//g')
  if [ $id_mount -le $id_snap ]; then
      log.info "No snapshot created since no changes since last snapshot. (Transaction id of $newestSnapshot is newer or equal to $target.)"
      exit $omit_error_code
  fi
}

函数:据 epoch 时间检查是否有变化

checktime_eq_normal(){
  if [ $snap_time_epoch == $target_time_epoch ]; then
      log.info "No snapshot created since timestamp of newest snapshot $newestSnapshot equal $target."
      exit $omit_error_code
  fi
}

函数:据 epoch 时间检查是否超出

checktime_duration(){
  if [ $(($snap_time_epoch + $time_duration)) -gt $cur_time_epoch ]; then
      log.info "Snapshot \"${store_path}/${snapname}\" not created as the latest snapshot \"$newestSnapshot\" is not older than \"$time_duration\" seconds."
      exit $omit_error_code
  fi
}

正式检查

if [ $time_duration -gt 0 ]; then
    newestSnapshot=`ls -dr ${store_path}/${snapname_pattern} 2>/dev/null| head -n 1`
    if [ ! -z "$newestSnapshot" -a -e "$newestSnapshot" ]; then
        snap_time_epoch=`stat -c "%Y" "${newestSnapshot}"`
        target_time_epoch=`stat -c "%Y" "${target}"`
        cur_time_epoch=`date +%s`
        if $use_transid
          then checktime_eq_transid
          else checktime_eq_normal
        fi
        checktime_duration
    fi
    # Force update of source timestamp to prevent outdated timestamps on the folders
    touch "${target}"
fi

正式处理快照

创建新快照

if $readonly ; then
  out=`btrfs subvol snapshot -r ${target} ${store_path}/${snapname} 2>&1`
else
  out=`btrfs subvol snapshot ${target} ${store_path}/${snapname} 2>&1`
fi
if [ $? -eq 0 ] ; then
    log.info "${out}"
else
    log.error "${out}";exit 1
fi

删除旧快照

ls -dr ${store_path}/${snapname_pattern} | tail -n +${keep_num} \
  | while read snap ; do
    out=`btrfs subvolume delete ${snap} 2>&1`
    if [ $? -eq 0 ] ; then
        log.info "${out}"
    else
        log.error "${out}";exit 1
    fi
done