Skip to content

Instantly share code, notes, and snippets.

@bigsmoke
Created August 12, 2009 19:58
Show Gist options
  • Save bigsmoke/166712 to your computer and use it in GitHub Desktop.
Save bigsmoke/166712 to your computer and use it in GitHub Desktop.
#!/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