Created
August 12, 2009 19:58
-
-
Save bigsmoke/166712 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/bin/bash | |
# | |
# linksafe is copyleft 2009 by Rowan Rodrik van der Molen and released under the General Public Licence v.3 | |
usage() { | |
cat <<EOF | |
Usage: $0 [OPTIONS] <source_dir> <destination_superdir> | |
$0 --help | |
For every file in <source_path>, this script creates a hardlink in a timestamped subdir of <destination_superdir>. | |
OPTIONS | |
--help Show this help | |
--retention-time <n> <units> Protect the files in <source_path> for <n> 'hours', 'days' or 'weeks'. | |
Without this options, nothing is ever deleted in <destination_superdir>. | |
--list-deleted Instead of creating a new 'copy', report what has been deleted in <source_path>. | |
--disk-usage Report how much extra disk space the linksafe takes. | |
--compare-last <n> <units> Report what has been deleted in the last <n> 'hours', 'days' or 'weeks'. | |
--undelete | |
--quiet Don't produce and output when saving. | |
EOF | |
} | |
end() { | |
message=$1 | |
status_code=$2 | |
if [ -z "$status_code" ]; then | |
status_code=0 | |
fi | |
if [ -n "$message" ]; then | |
if [ $status_code -eq 0 ]; then | |
echo -e "\e[1;33m$message\e[0m" >&2 # Errors in red | |
else | |
echo -e "\e[1;31m$message\e[0m" >&2 # Other messages in yellow | |
fi | |
fi | |
if [ "$tmp_dir" ]; then | |
rm -rf "$tmp_dir" | |
fi | |
exit $status_code | |
} | |
end_signal() { | |
end "Interup caught; cleaning temporary files..." 1 | |
} | |
die() { | |
end $1 1 | |
} | |
usage_error() { | |
if [ "$1" ]; then echo -e "\e[1;31m$1\e[0m" >&2; fi | |
usage >&2 | |
exit 2 | |
} | |
disk_usage() { | |
echo -e "\e[1mOutput:\e[0m All numbers indicate the extra disk space that is required to\nkeep the backup for that date, with the bold numbers indicating totals.\n" | |
cd $dst_superdir | |
subdirs_expired='' | |
subdirs_retained='' | |
# Traverse all subdirs of $dst_superdir in reverse order (the most recent first) | |
while read subdir; do | |
subdirs_computed="$subdirs_computed $subdir" | |
subdir_time=`echo $subdir|sed -e 's/@/ /'` | |
subdir_utime=`date --date="$subdir_time" +%s` | |
# Does the current subdir need to be retained? | |
if [ $subdir_utime -ge $expiration_utime ]; then | |
subdirs_retained="$subdirs_retained $subdir" | |
echo -e "\e[0;32m$subdir_time\t\e[0;32m"`subdir_usage $subdir` | |
# Has the current subdir been expired? | |
else | |
# Are we at the first subdir that is expired? | |
if [ -z "$subdirs_expired" ]; then | |
echo -ne "\e[1;33m"`date --date=@$expiration_utime '+%Y-%m-%d %H:%M'`"\t" | |
echo -e `subdir_usage $subdirs_retained`" will still be used with this retention time" | |
fi | |
echo -e "\e[0;31m$subdir_time\t\e[0;31m"`subdir_usage $subdir` | |
subdirs_expired="$subdirs_expired $subdir" | |
fi | |
done < <(ls -r) | |
if [ -n "$subdirs_expired" ]; then | |
echo -e "\t\t\t\e[1;31m"`freed_when_subdirs_expire`" total will be freed with the given retention time" | |
else | |
echo -e "\t\t\t\e[1;32m"`subdir_usage $subdirs_expired $subdirs_retained`" total is currently in use for the linksafe" | |
fi | |
} | |
freed_when_subdirs_expire() { | |
find $subdirs_expired -type f -printf '%i\n' | while read inum; do | |
if [ ! "`find $src_dir $subdirs_retained -inum $inum`" ]; then | |
find "$dst_superdir" -inum $inum -print0 | |
fi | |
done | du --human-readable --total --summarize --files0-from=- | tail -n 1 | cut -f 1 | |
} | |
subdir_usage() { | |
find $* -type f -printf '%i\n' | while read inum; do | |
if [ ! `find $src_dir -inum $inum` ]; then | |
find "$dst_superdir" -inum $inum -print0 | |
fi | |
done | du --human-readable --total --summarize --files0-from=- | tail -n 1 | cut -f 1 | |
} | |
list_deleted() { | |
cd $dst_superdir | |
ls | while read subdir; do | |
test $retention_time -ne 0 && subdir_exceeded_retention_time $subdir && continue | |
cd "$subdir" 2>/dev/null || continue | |
find . -type f -printf '%i %n %h/%f\n' >> "$tmp_dir/combined_file_list" | |
done | |
sort --unique < "$tmp_dir/combined_file_list" | while read saved_file_entry; do | |
saved_file_inum=`echo $saved_file_entry|cut -f 1 -d ' '` | |
if [ -z `find $src_dir -inum $saved_file_inum` ]; then | |
echo "$saved_file_entry" | sed -e 's#^\d+ \./##' | |
fi | |
done | |
} | |
parse_time() { | |
n=$1 | |
units=$2 | |
multiplier='' | |
case "$units" in | |
"second"|"seconds") | |
multiplier=1 | |
;; | |
"minute"|"minutes") | |
multiplier=60 | |
;; | |
"hour"|"hours") | |
multiplier=3600 | |
;; | |
"day"|"days") | |
multiplier=86400 | |
;; | |
"week"|"weeks") | |
multiplier=604800 | |
;; | |
*) | |
die "Invalid time unit '$units' given." | |
;; | |
esac | |
expr $multiplier \* $n | |
} | |
subdir_exceeded_retention_time() { | |
dirtime=`echo $1|sed -e 's/@/ /'` # Example: "2009-08-16@12:32" | |
dirutime=`date --date="$dirtime" +%s` | |
[ $dirutime -lt $expiration_utime ] | |
} | |
save() { | |
cd $src_dir | |
find . $find_options -mindepth 1 -type d -printf 'mkdir --mode=%m "%h/%f"; chown %U:%G "%h/%f"\n' > "$tmp_dir/source_dir_list" | |
find . $find_options -type f -o -type l > "$tmp_dir/source_file_list" | |
dst_dir="$dst_superdir/`date +'%F@%H:%M'`" | |
if [ -d "$dst_dir" ]; then | |
die "You must wait until this minute has gone by before running this script again." | |
fi | |
mkdir "$dst_dir" | |
cd "$dst_dir" | |
echo -e "\e[1;33mCreating directories...\e[0m" | |
cat "$tmp_dir/source_dir_list" | while read command; do | |
eval "$command" | |
done | |
echo -e "\e[1;33mCreating hard links...\e[0m" | |
cat "$tmp_dir/source_file_list" | while read relative_file; do | |
ln "$src_dir/$relative_file" "$relative_file" | |
done | |
purge_expired_subdirs | |
} | |
# Clean out old subdirs if a retention time was given | |
purge_expired_subdirs() { | |
if [ $retention_time -ne 0 ]; then | |
cd $dst_superdir | |
ls | while read subdir | |
do | |
if [ -d $subdir ] && subdir_exceeded_retention_time $subdir; then | |
echo -e "\e[1;34m$subdir\e[37m is older than $n_time_units $time_units; \e[33mdeleting...\e[0m" | |
rm -rf $subdir | |
fi | |
done | |
fi | |
} | |
demo() { | |
echo | |
} | |
self_test() { | |
fake_dir=`mktemp -d` | |
fake_backup_dir=`mktemp -d` | |
fake_subdir_1="$fake_dir/Someone's idea of a funny name (1)/.humor me" | |
mkdir -p "$fake_subdir_1" | |
fake_file_1="$fake_subdir_1/hidden_away_deep" | |
dd if=/dev/zero of="$fake_file_1" bs=1024 count=1024 | |
fake_symlink_1="$fake_dir/i'm not hidden at all" | |
ln -s "$fake_file_1" "$fake_symlink_1" | |
$0 "$fake_dir" "$fake_backup_dir" | |
# Test whether everything is literally copied | |
# Test disk usage | |
fake_file_2="$fake_dir/big" | |
dd if=/dev/zero of="$fake_file_2" bs=1024 count=2048 | |
$0 "$fake_dir" "$fake_backup_dir" | |
# Test disk usage with retention time | |
} | |
PATH=/usr/bin:/bin | |
if [ ! $UID -eq 0 ]; then | |
die "You're not root. Go away." | |
fi | |
find_options="-mount" | |
action=save | |
time_units='' | |
n_time_units='' | |
retention_time=0 | |
current_utime=`date +%s` | |
expiration_utime=0 # The moment before which backups don't need to be retained | |
# Parse options | |
while [ $# -gt 0 ]; do | |
case "$1" in | |
--help) | |
usage | |
exit 0 | |
;; | |
--self-test) | |
action=self_test | |
[ $# -eq 1 ] || usage_error "Too many arguments for --self-test." | |
;; | |
--list-deleted) | |
action=list_deleted | |
;; | |
--disk-usage) | |
action=disk_usage | |
;; | |
--purge-expired|--free-expired) | |
action=purge_expired_subdirs | |
;; | |
--retention-time) | |
[ $# -ge 4 ] || usage_error "Insufficient number of arguments." | |
n_time_units=$2 | |
time_units=$3 | |
retention_time=`parse_time $2 $3` | |
expiration_utime=`expr $current_utime - $retention_time` | |
shift 2 | |
;; | |
--) # Useless, but otherwise Wiebe will gloat | |
shift | |
break | |
;; | |
-*) | |
usage_error "Unknown option ‘$1’" | |
;; | |
*) | |
break; | |
;; | |
esac | |
shift | |
done | |
case $action in | |
save|purge_expired_subdirs|disk_usage|list_deleted) | |
[ $# -eq 2 ] || usage_error "<source_path> and <destination_path> are both required." | |
src_dir=$1 | |
dst_superdir=$2 | |
# Make relative paths absolute | |
if [ "${src_dir:0:1}" != "/" ]; then src_dir="$PWD/$src_dir"; fi | |
if [ "${dst_superdir:0:1}" != "/" ]; then dst_superdir="$PWD/$dst_superdir"; fi | |
[ -d "$src_dir" ] || usage_error "<source_path> ($src_dir) is not a directory." | |
[ -d "$dst_superdir" ] || usage_error "<destination_path> ($dst_superdir) is not a directory." | |
[ "$src_dir" -ef "$dst_superdir" ] && usage_error "<source_path> and <destination_path> are the same." | |
;; | |
esac | |
trap "end_signal" INT TERM SIGHUP | |
tmp_dir=`mktemp -d` | |
test $? || die "Creating temporary dir failed." | |
$action | |
end | |
# vim: set expandtab tabstop=4 shiftwidth=4 syntax=sh: |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment