392 lines
12 KiB
Bash
Executable File
392 lines
12 KiB
Bash
Executable File
#!/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 <https://www.gnu.org/licenses/gpl-3.0-standalone.html>.
|
|
#
|
|
# SPDX-License-Identifier: GPL-3.0-or-later <https://spdx.org/licenses/GPL-3.0-or-later.html>
|
|
#
|
|
#%MAN_BEGIN%
|
|
# NAME
|
|
# sync-view.sh - list file versions from rsync.sh backups.
|
|
#
|
|
# SYNOPSIS
|
|
# sync-view.sh [OPTIONS] TARGET
|
|
#
|
|
# DESCRIPTION
|
|
# List TARGET versions from a sync.sh backup directory.
|
|
#
|
|
# OPTIONS
|
|
# -1, --unique
|
|
# Skip duplicate files. This option do not apply if TARGET is a
|
|
# directory.
|
|
#
|
|
# -a, --absolute-target
|
|
# Do not try to resolve TARGET path. By default, the script will try to
|
|
# guess TARGET absolute path. This is not possible if current system is
|
|
# different from the one from which the backup was made, or if some
|
|
# path component are missing or were changed.
|
|
# If this option is used, TARGET must be an absolute path as it was on
|
|
# backuped machine.
|
|
#
|
|
# -b, --backupdir=DIR
|
|
# DIR is the local path where the backups can be found. It can be a
|
|
# network mount, or the destination directory if the backup was local.
|
|
# This option is mandatory.
|
|
#
|
|
# -c, --config
|
|
# A sync.sh configuration file where the script could find variables
|
|
# SOURCEDIR (option '-r') and BACKUPDIR (option '-b').
|
|
# If this option is missing, the script will try to find a .syncrc file
|
|
# in DIR/daily-01 directory, where DIR is the local path of backups
|
|
# (option -b).
|
|
#
|
|
# -d, --destdir
|
|
# Directory which will hold links to actual files. It will be created
|
|
# if non-existant. If this option is missing, 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
|
|
#
|
|
# The simplest invocation: the versions of users' .bashrc will be retrieved
|
|
# in backups from /mnt/backup. A /mnt/backup/daily-01/.syncrc must exist.
|
|
# $ sync-view.sh -b /mnt/backup ~/.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 /tmp/test ~/.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
|
|
RESOLVETARGET=y # resolve TARGET
|
|
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][-1ahmv] 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
|
|
}
|
|
|
|
# filetype() - get file type
|
|
#
|
|
# $1: the file to check
|
|
#
|
|
# @return: 0, output a string with file type on stdout.
|
|
filetype() {
|
|
local file="$1" type="unknown"
|
|
if [[ ! -e "$file" ]]; then
|
|
type="missing"
|
|
elif [[ -h "$file" ]]; then
|
|
type="symlink"
|
|
elif [[ -f "$file" ]]; then
|
|
type="file"
|
|
elif [[ -d "$file" ]]; then
|
|
type="directory"
|
|
elif [[ -p "$file" ]]; then
|
|
type="fifo"
|
|
elif [[ -b "$file" || -c "$file" ]]; then
|
|
type="device"
|
|
fi
|
|
printf "%s" "$type"
|
|
return 0
|
|
}
|
|
|
|
# command-line parsing / configuration file read.
|
|
parse_opts() {
|
|
# short and long options
|
|
local sopts="1ab:c:d:hmr:vx:"
|
|
local lopts="unique,absolute-target,backupdir:,config:,destdir:,help,man,root:,verbose,exclude:"
|
|
local tmp tmp_destdir="" tmp_destdir="" tmp_rootdir="" tmp_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
|
|
;;
|
|
'-a'|'--absolute-target')
|
|
RESOLVETARGET=""
|
|
;;
|
|
'-b'|'--backupdir')
|
|
tmp_backupdir="$2"
|
|
shift
|
|
;;
|
|
'-c'|'--config')
|
|
# 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.
|
|
tmp_config="$2"
|
|
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 argument (searched file).
|
|
if (( $# != 1 )); then
|
|
usage
|
|
exit 1
|
|
fi
|
|
TARGET="$1"
|
|
[[ -z $RESOLVETARGET ]] || TARGET="$(realpath -L "$TARGET")"
|
|
|
|
# if $config is not set, look for .syncrc in BACKUPDIR
|
|
tmp_config=${tmp_config:-$tmp_backupdir/daily-01/.syncrc}
|
|
if [[ -z "$tmp_config" ]]; then
|
|
printf "%s: Missing configuration file.\n" "$CMDNAME"
|
|
exit 10
|
|
elif [[ ! -r "$tmp_config" ]]; then
|
|
printf "%s: Cannot open %s file. Exiting.\n" "$CMDNAME" "$tmp_config"
|
|
exit 9
|
|
fi
|
|
|
|
# shellcheck source=sync-conf-example.sh
|
|
source "$tmp_config"
|
|
[[ -n "$SOURCEDIR" ]] && ROOTDIR="$SOURCEDIR"
|
|
|
|
[[ -n "$tmp_backupdir" ]] && BACKUPDIR="$tmp_backupdir"
|
|
[[ -n "$tmp_destdir" ]] && TARGETDIR="$tmp_destdir"
|
|
[[ -n "$tmp_rootdir" ]] && ROOTDIR="$tmp_rootdir"
|
|
return 0
|
|
}
|
|
|
|
check_paths() {
|
|
local tmp
|
|
|
|
[[ -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." "$TARGETDIR"
|
|
mkdir "$TARGETDIR"
|
|
fi
|
|
else
|
|
tmp="$(basename "$TARGET")"
|
|
TARGETDIR="$(mktemp -d /tmp/"$tmp"-XXXXXXXX)"
|
|
log "%s target directory created." "$TARGETDIR"
|
|
fi
|
|
log "ROOTDIR=[%s]" "$ROOTDIR"
|
|
log "BACKUPDIR=[%s]" "$BACKUPDIR"
|
|
log "TARGETDIR=[%s]" "$TARGETDIR"
|
|
log "TARGET=[%s]" "$TARGET"
|
|
for var in BACKUPDIR TARGETDIR; do
|
|
[[ $var = ROOTDIR && -z $RESOLVETARGET ]] && continue
|
|
if [[ ! -d "${!var}" ]]; 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" "$TARGETDIR"
|
|
exit 1
|
|
fi
|
|
# remove existing files
|
|
if [[ -n "$(ls -A .)" ]]; then
|
|
log "Cleaning existing directory %s." "$TARGETDIR"
|
|
for target in *; do
|
|
rm "$target"
|
|
done
|
|
fi
|
|
return 0
|
|
}
|
|
|
|
parse_opts "$@"
|
|
check_paths
|
|
|
|
# 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
|
|
_tmp=${TARGET#"$ROOTDIR"}
|
|
[[ $_tmp =~ ^/.*$ ]] || _tmp="/$_tmp"
|
|
src="$file$_tmp"
|
|
#printf "src=%s\n" "$src"
|
|
if [[ ! -e $src ]]; then
|
|
log "Skipping non-existing %s" "$src"
|
|
continue
|
|
fi
|
|
#ls -li "$src"
|
|
|
|
# last modification time in seconds since epoch
|
|
inode=$(stat --dereference --printf="%i" "$src")
|
|
date=$(stat --printf="%Y" "$src")
|
|
date_backup=$(stat --dereference --printf="%Y" "$file")
|
|
# 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 %s inode %s (%s)" "$file" "$inode" "$target"
|
|
ln -fs "$src" "$TARGETDIR/$target"
|
|
else
|
|
log "Skipping duplicate inode %s (%s)" "$inode" "$target"
|
|
fi
|
|
INODES[$inode]=${INODES[$inode]:-$date}
|
|
INODES[backup-$inode]=${INODES[backup-$inode]:-$date_backup}
|
|
done
|
|
|
|
if [[ -n "$(ls -A .)" ]]; then
|
|
printf "backup date (backup)|last changed|inode|size|perms|type|path\n"
|
|
# for file in {dai,week,month,year}ly-[0-9][0-9]; do
|
|
for symlink in *; do
|
|
file=$(readlink "$symlink")
|
|
#printf "file=<%s> link=<%s>\n" "$file" "$symlink" >&2
|
|
inode=$(stat --printf="%i" "$file")
|
|
type=$(filetype "$file")
|
|
#links=$(stat --printf="%h" "$file")
|
|
date=$(date --date="@${INODES[$inode]}" "+%Y-%m-%d %H:%M")
|
|
backup_date=$(date --date="@${INODES[backup-$inode]}" "+%Y-%m-%d %H:%M")
|
|
size=$(stat --printf="%s" "$file")
|
|
perms=$(stat --printf="%A" "$file")
|
|
printf "%s (%s)|" "$backup_date" "$symlink"
|
|
printf "%s|" "$date"
|
|
#printf "%s|" "$links"
|
|
printf "%s|" "$inode"
|
|
printf "%s|" "$size"
|
|
printf "%s|" "$perms"
|
|
printf "%s|" "$type"
|
|
printf "%s\n" "$file"
|
|
# ls -lrt "$TARGETDIR"
|
|
done | sort -r
|
|
fi | column -t -s\|
|
|
|
|
printf "temporary files directory is: %s\n" "$PWD"
|
|
exit 0
|