From d8dde368d2b5f51c9b37c4fcee041a5963f59355 Mon Sep 17 00:00:00 2001 From: Bruno Raoult Date: Wed, 13 Jul 2022 14:29:21 +0200 Subject: [PATCH] sync-view.sh: first version --- bash/sync-conf-example.sh | 6 +- bash/sync-view.sh | 322 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 327 insertions(+), 1 deletion(-) create mode 100755 bash/sync-view.sh diff --git a/bash/sync-conf-example.sh b/bash/sync-conf-example.sh index 07a0208..a877717 100644 --- a/bash/sync-conf-example.sh +++ b/bash/sync-conf-example.sh @@ -18,7 +18,7 @@ # The only mandatory ones are SOURCEDIR, SERVER, and DESTDIR. ###### source directory full path, destination server and path. -###### SERVER could user@host, or "local" if local machine +###### SERVER could be user@host, or "local" if local machine # SOURCEDIR="" # SERVER="" # DESTDIR="" @@ -26,6 +26,10 @@ export SOURCEDIR=/example-srcdir export SERVER=root@backuphost export DESTDIR=/mnt/nas1/example-destdir +###### backups mount point on local machine. +###### it is used by sync-view.sh only. +export BACKUPDIR=/mnt/backup-example-srcdir + ###### backups to keep # NYEARS=3 # NMONTHS=12 diff --git a/bash/sync-view.sh b/bash/sync-view.sh new file mode 100755 index 0000000..cd50ce9 --- /dev/null +++ b/bash/sync-view.sh @@ -0,0 +1,322 @@ +#!/usr/bin/env bash +# +# sync-view.sh - view file versions in a sync.sh backup directory. +# +# (C) Bruno Raoult ("br"), 2007-2022 +# Licensed under the GNU General Public License v3.0 or later. +# Some rights reserved. See COPYING. +# +# You should have received a copy of the GNU General Public License along with this +# program. If not, see . +# +# SPDX-License-Identifier: GPL-3.0-or-later +# +#%MAN_BEGIN% +# NAME +# sync-view.sh - list file versions from rsync.sh backups. +# +# SYNOPSIS +# sync-view.sh [OPTIONS] FILE +# +# DESCRIPTION +# List FILE versions from a sync.sh backup directory. +# +# OPTIONS +# -1, --unique +# Skip duplicate files. This option do not apply if FILE is a +# directory. +# +# -b, --backupdir=DIR +# DIR is the local mount point where the backups can be found. It can +# be a network mount, or the destination directory if the backup was +# local. +# +# -c, --config +# A sync.sh file where +# if non-existant. By default, a temporary directory will be created +# in /tmp. +# +# -d, --destdir +# Directory which will hold links to actual files. It will be created +# if non-existant. By default, a temporary directory will be created +# in /tmp. +# +# -h, --help +# Display short help and exit. +# +# -m, --man +# Display a "man-like" description and exit. +# +# -r, --root=DIR +# DIR is the path of the backup source. If '-c' option is used, the +# variable SOURCEDIR will be used. By default '/'. +# +# -v, --verbose +# Print messages on what is being done. +# +# -x, --exclude=REGEX +# Filenames matching REGEX (with relative path to backup directory, +# as specified with '-b' option) will be excluded. This option can be +# useful +# +# EXAMPLES +# The next command will list all .bashrc versions for current user, from +# backups in /mnt/backup. yearly and monthly-03 to monthly-09 are +# excluded. Source directory (-r) of backups are taken from sync.sh +# configuration file named s.conf. A temporary directory will be created +# in /mnt to handle links to actual files. +# $ sync-view.sh -c s.conf -b /mnt/backup -x "^(yearly|monthly-0[3-9]).*$" ~/.bashrc +# +# Links to user's .bashrc backups will be put in /tmp/test. Files are in +# /mnt/backup, which contains backups of /export directory. The /tmp/test +# directory will be created if necessary. +# $ sync-view.sh -r /export -b /mnt/backup -d /tmptest ~/.bashrc +# +# AUTHOR +# Bruno Raoult. +# +#%MAN_END% + +# internal variables, cannot (and *should not*) be changed unless you +# understand exactly what you do. +SCRIPT="$0" # full path to script +CMDNAME=${0##*/} # script name +HOSTNAME="$(hostname)" + +ROOTDIR="/" # root of backup source +BACKUPDIR="" # the local view of backup dirs +TARGETDIR="" # temp dir to hold links + +TARGET="" # the file/dir to find +UNIQUE="" # omit duplicate files +EXCLUDE="" # regex for files to exclude +VERBOSE="" # -v option +declare -A INODES # inodes table (for -1 option) + +# error management +set -o errexit +#set -o xtrace + +usage() { + printf "usage: %s [-b BACKUPDIR][-c CONF][-d DSTDIR][-r ROOTDIR][-x EXCLUDE][-1hmv] file\n" "$CMDNAME" + return 0 +} + +man() { + sed -n '/^#%MAN_BEGIN%/,/^#%MAN_END%$/{//!s/^#[ ]\{0,1\}//p}' "$SCRIPT" | more +} + +# log function +# parameters: +# -l, -s: long, or short prefix (default: none). Last one is used. +# -t: timestamp +# -n: no newline +# This function accepts either a string, either a format string followed +# by arguments : +# log -s "%s" "foo" +# log -s "foo" +log() { + local timestr="" prefix="" newline=y todo OPTIND + [[ -z $VERBOSE ]] && return 0 + while getopts lsnt todo; do + case $todo in + l) prefix=$(printf "*%.s" {1..30}) + ;; + s) prefix=$(printf "*%.s" {1..5}) + ;; + n) newline=n + ;; + t) timestr=$(date "+%F %T%z ") + ;; + *) ;; + esac + done + shift $((OPTIND - 1)) + [[ $prefix != "" ]] && printf "%s " "$prefix" + [[ $timestr != "" ]] && printf "%s" "$timestr" + # shellcheck disable=SC2059 + printf "$@" + [[ $newline = y ]] && printf "\n" + return 0 +} + +# command-line parsing / configuration file read. +parse_opts() { + # short and long options + local sopts="1b:c:d:hmr:vx:" + local lopts="unique,backupdir:,config:,destdir:,help,man,root:,verbose,exclude:" + local tmp tmp_destdir="" tmp_destdir="" tmp_rootdir="" config + + if ! tmp=$(getopt -o "$sopts" -l "$lopts" -n "$CMD" -- "$@"); then + log "Use '$CMD --help' or '$CMD --man' for help." + exit 1 + fi + + eval set -- "$tmp" + + while true; do + case "$1" in + -1|--unique) + UNIQUE=yes + ;; + '-b'|'--backupdir') + tmp_backupdir="$2" + shift + ;; + '-c'|'--config') + config="$2" + if [[ ! -r "$config" ]]; then + printf "%s: Cannot open %s file. Exiting.\n" "$CMDNAME" "$config" + exit 9 + fi + # shellcheck source=sync-conf-example.sh + source "$config" + [[ -n "$SOURCEDIR" ]] && ROOTDIR="$SOURCEDIR" + shift + ;; + '-d'|'--destdir') + tmp_destdir="$2" + shift + ;; + '-h'|'--help') + usage + exit 0 + ;; + '-m'|'--man') + man + exit 0 + ;; + '-r'|'--rootdir') + tmp_rootdir="$2" + shift + ;; + '-v'|'--verbose') + VERBOSE=yes + ;; + '-x'|'--exclude') + EXCLUDE="$2" + shift + ;; + '--') + shift + break + ;; + + *) + usage + log 'Internal error!' + exit 1 + ;; + esac + shift + done + + # Now check remaining arguments (configuration file and searched file). + # The configuration file contains the variable SOURCEDIR, which will allow + # to find the relative path of TARGET in backup tree. + # it may also contain BACKUPDIR variable, which the local root of backup + # tree. + (( $# != 1 )) && ! usage + TARGET="$(realpath -L "$1")" + + [[ -n "$tmp_backupdir" ]] && BACKUPDIR="$tmp_backupdir" + [[ -n "$tmp_destdir" ]] && TARGETDIR="$tmp_destdir" + [[ -n "$tmp_rootdir" ]] && ROOTDIR="$tmp_rootdir" + return 0 +} + +check_dirs() { + local dir tmp + + log "ROOTDIR=[%s]" "$ROOTDIR" + log "BACKUPDIR=[%s]" "$BACKUPDIR" + log "TARGETDIR=[%s]" "$TARGETDIR" + log "FILE=[%s]" "$TARGET" + + [[ -z "$BACKUPDIR" ]] && printf "%s: backup directory is not set.\n" "$CMDNAME" && \ + ! usage + [[ -z "$ROOTDIR" ]] && printf "%s: source directory is not set.\n" "$CMDNAME" && \ + ! usage + if [[ -n "$TARGETDIR" ]]; then + if [[ ! -e $TARGETDIR ]]; then + log "Creating destination directory %s." "$DESTDIR" + mkdir "$TARGETDIR" + fi + else + tmp="$(basename "$TARGET")" + TARGETDIR="$(mktemp -d /tmp/"$tmp"-XXXXXXXX)" + log "%s target directory created." "$TARGETDIR" + fi + for var in ROOTDIR BACKUPDIR TARGETDIR; do + dir=$var + if [[ ! -d "${!dir}" ]]; then + printf "%s is not a directory.\n" "$var" + exit 1 + fi + done + if ! pushd "$TARGETDIR" > /dev/null; then + printf "cannot change to directory %s.\n" "$DESTDIR" + exit 1 + fi + # remove existing files + if [[ -n "$(ls -A .)" ]]; then + log "Cleaning existing directory %s." "$DESTDIR" + for target in *; do + rm "$target" + done + fi +} + +parse_opts "$@" +check_dirs + +# add missing directories +declare -a DIRS +DIRS=("$BACKUPDIR"/{dai,week,month,year}ly-[0-9][0-9]) +log "DIRS=%s" "${DIRS[*]}" + +for file in "${DIRS[@]}"; do + # src is file/dir in backup tree + src="$file${TARGET#"$ROOTDIR"}" + if [[ ! -e $src ]]; then + log "Skipping non-existing %s" "$src" + continue + fi + #ls -lLi "$src" + + # last modification time in seconds since epoch + inode=$(stat --dereference --printf="%i\n" "$src") + date=$(stat --dereference --printf="%Y\n" "$src") + # target is daily-01, etc... + #target=$(date --date="@$date" "+%Y-%m-%d %H:%M")" - ${file#"$BACKUPDIR/"}" + target="${file#"$BACKUPDIR/"}" + + #printf "target=[%s] src=[%s]\n" "$target" "$src" + if [[ -n $EXCLUDE && $target =~ $EXCLUDE ]]; then + log "Skipping %s\n" "$file" + continue + fi + if [[ -z $UNIQUE || ! -v INODES[$inode] ]]; then + log "Adding inode %s (%s)" "$inode" "$target" + ln -fs "$src" "$TARGETDIR/$target" + else + log "Skipping duplicate inode %s (%s)" "$inode" "$target" + fi + INODES[$inode]=${INODES[$inode]:-$date} +done + +{ + printf "mod time|backup|inode|size|perms|path\n" + # for file in {dai,week,month,year}ly-[0-9][0-9]; do + for file in *; do + inode=$(stat --dereference --printf="%i" "$file") + date=$(date --date="@${INODES[$inode]}" "+%Y-%m-%d %H:%M") + size=$(stat --dereference --printf="%s" "$file") + perms=$(stat --dereference --printf="%A" "$file") + path=$(readlink "$file") + printf "%s|%s|%s|%s|%s|%s\n" "$date" "$file" "$inode" "$size" "$perms" "$path" + # ls -lrt "$TARGETDIR" + done | sort -r +} | column -t -s\| + +exit 0