#!/bin/bash # Includes . $(miniade) || { echo "${0##*/}: ERROR: miniade failed (hint: run 'miniade' to see error)" >&2; exit 1; } # Configurable stuff #DBFILE=/tmp/$PROGNAME.sqlite # Other globals LOTS_OF_EQUALS="==============================================================================================================================================" main() { local MY_ARGS local PROGNAME # Defaults for options DESIRED_MODES=() # Process options special_opts_handler() { case $1 in --scan) DESIRED_MODES=( "${DESIRED_MODES[@]}" scan ) ;; --compare) DESIRED_MODES=( "${DESIRED_MODES[@]}" compare ) ;; --freeze) DESIRED_MODES=( "${DESIRED_MODES[@]}" freeze ) ;; --list) DESIRED_MODES=( "${DESIRED_MODES[@]}" list ) ;; --progname=*) miniade_set_progname ${1#*=} ;; *) return 1 ;; esac } miniade_process_options --special-opts-handler=special_opts_handler --help-handler=help MY_ARGS "$@" && set -- "${MY_ARGS[@]}" # Process arguments (delegated) # Sanity checks and derivations miniade_get_progname PROGNAME [ ${#DESIRED_MODES[*]} -gt 0 ] || miniade_bad_usage if [ -w / ]; then STATE_DIR=/var/local/$PROGNAME else STATE_DIR=$HOME/.cache/$PROGNAME fi # Guts (delegate) for MODE in scan compare freeze list; do if wordin $MODE "${DESIRED_MODES[@]}"; then $MODE "$@" fi done } wordin() { local FIRSTWORD NEXTWORD FOUND FIRSTWORD=$1 shift FOUND=false for NEXTWORD in "$@"; do [ "X$NEXTWORD" != "X$FIRSTWORD" ] || { FOUND=true; break; } done $FOUND } scan() { local ROOT_DIR FROZEN_FILE SCANNED_FILE PROGNAME # Process arguments [ $# = 1 ] || miniade_bad_usage ROOT_DIR=$1 # Sanity checks and derivations [[ $ROOT_DIR =~ ^/ ]] || miniade_error "$ROOT_DIR: not absolute" ( cd $ROOT_DIR 2>/dev/null ) || miniade_error "$ROOT_DIR: can't access" FROZEN_FILE=$STATE_DIR/$(zencode <<<"$ROOT_DIR")-frozen.md5sum SCANNED_FILE=$STATE_DIR/$(zencode <<<"$ROOT_DIR")-scanned.md5sum miniade_get_progname PROGNAME # Guts miniade_lock /tmp/$PROGNAME.lock || miniade_error "locked" mkdir -p $STATE_DIR miniade_debug 10 "scan: running find ..." # Alphabetical order scan (easier to read by me while being written by script). find $ROOT_DIR -type f -print0 | sort -z | xargs -r0 md5sum > $SCANNED_FILE.$$ || true miniade_debug 10 "scan: moving result to $SCANNED_FILE ..." mv $SCANNED_FILE.$$ $SCANNED_FILE miniade_unlock /tmp/$PROGNAME.lock } zencode() { sed -e 's/Z/Z5a/g' -e 's/\//Z2f/g' } zdecode() { sed -e 's/Z2f/\//g' -e 's/Z5a/Z/g' } list() { local ROOT_DIR FROZEN_FILE SCANNED_FILE ROOT_DIRS # Process arguments [ $# = 0 ] || miniade_bad_usage # Sanity checks and derivations # Guts mkdir -p $STATE_DIR ROOT_DIRS=( $(ls $STATE_DIR | sed -r 's/-(scanned|frozen)\.md5sum$//' | sort -u | zdecode) ) for ROOT_DIR in "${ROOT_DIRS[@]}"; do FROZEN_FILE=$STATE_DIR/$(zencode <<<"$ROOT_DIR")-frozen.md5sum SCANNED_FILE=$STATE_DIR/$(zencode <<<"$ROOT_DIR")-scanned.md5sum if [ -f $SCANNED_FILE ] && [ -f $FROZEN_FILE ]; then COMMENT="frozen based on a scan that completed at $(stat -c %y $FROZEN_FILE)" elif [ -f $SCANNED_FILE ] && [ ! -f $FROZEN_FILE ]; then COMMENT="not frozen but scan completed at $(stat -c %y $SCANNED_FILE)" elif [ ! -f $SCANNED_FILE ] && [ -f $FROZEN_FILE ]; then COMMENT="frozen based on a scan that completed at $(stat -c %y $FROZEN_FILE)" elif [ ! -f $SCANNED_FILE ] && [ ! -f $FROZEN_FILE ]; then COMMENT="odd state!" fi printf "%-40s %s\\n" "$ROOT_DIR:" "$COMMENT" done } compare() { local ROOT_DIR FROZEN_FILE SCANNED_FILE PROGNAME # Process arguments [ $# = 1 ] || miniade_bad_usage ROOT_DIR=$1 # Sanity checks and derivations [[ $ROOT_DIR =~ ^/ ]] || miniade_error "$ROOT_DIR: not absolute" FROZEN_FILE=$STATE_DIR/$(zencode <<<"$ROOT_DIR")-frozen.md5sum SCANNED_FILE=$STATE_DIR/$(zencode <<<"$ROOT_DIR")-scanned.md5sum miniade_get_progname PROGNAME [ -f $SCANNED_FILE ] || miniade_error "no scanned file exists yet (hint: do you need to run '$PROGNAME --scan $ROOT_DIR'?)" [ -f $FROZEN_FILE ] || miniade_error "no frozen file exists yet (hint: do you need to run '$PROGNAME --freeze $ROOT_DIR'?)" # Guts [ "X$DBFILE" = X ] || rm -f $DBFILE { echo "CREATE TABLE scanned_files (checksum TEXT, name TEXT, PRIMARY KEY (name));" echo "CREATE INDEX scanned_files_checksum_index ON scanned_files(checksum);" echo "CREATE INDEX scanned_files_name_index ON scanned_files(name);" echo "CREATE TABLE frozen_files (checksum TEXT, name TEXT, PRIMARY KEY (name));" echo "CREATE INDEX frozen_files_checksum_index ON frozen_files(checksum);" echo "CREATE INDEX frozen_files_name_index ON frozen_files(name);" echo "BEGIN TRANSACTION;" # md5sum puts a backslash at the beginning of the line (directly in front of # the checksum if the filename contains a backslash). Single quotes need to # escaped, which, in the case of quotes is actually *two* single quotes. sed -r -e 's/^\\//' -e "s/'/''/g" -e "s/^([0-9a-f]{32}) (.*)/INSERT INTO scanned_files VALUES ('\\1', '\\2');/" $SCANNED_FILE sed -r -e 's/^\\//' -e "s/'/''/g" -e "s/^([0-9a-f]{32}) (.*)/INSERT INTO frozen_files VALUES ('\\1', '\\2');/" $FROZEN_FILE echo "END TRANSACTION;" echo "CREATE VIEW interesting_files as select *,'frozen' as source from (select * from frozen_files except select * from scanned_files) union select *,'scanned' as source from (select * from scanned_files except select * from frozen_files);" # Moved files are those with a 'frozen' and a 'scanned' entry for the same # checksum but with different names. echo "CREATE view moved_files as SELECT i1.checksum, i1.name as name_frozen, i2.name as name_scanned FROM interesting_files as i1, interesting_files as i2 where i1.checksum = i2.checksum and i1.source = 'frozen' and i2.source = 'scanned';" echo ".output /tmp/$PROGNAME.$$.moved" echo ".width 140" echo "SELECT name_frozen || ' --> ' || name_scanned as 'old name --> new name' FROM moved_files order by name_frozen;" echo ".output" # Modified files are the opposite: those with a 'frozen' and a 'scanned' entry # for the same name but with different checksums. echo "CREATE VIEW modified_files as SELECT i1.checksum as checksum_frozen, i2.checksum as checksum_scanned, i1.name FROM interesting_files as i1, interesting_files as i2 where i1.name = i2.name and i1.source = 'frozen' and i2.source = 'scanned';" echo ".output /tmp/$PROGNAME.$$.modified" echo ".width 140" echo "SELECT name FROM modified_files order by name;" echo ".output" # Added files are not moved or modified and have a 'scanned' entry but no # 'frozen' entry. echo "CREATE VIEW added_files as select checksum, name from (select * from interesting_files except select * from (select checksum_frozen as checksum, name, 'frozen' as source from modified_files union select checksum_scanned as checksum, name, 'scanned' as source from modified_files union select checksum, name_frozen as name, 'frozen' as source from moved_files union select checksum, name_scanned as name, 'scanned' as source from moved_files)) where source = 'scanned';" echo ".output /tmp/$PROGNAME.$$.added" echo ".width 140" echo "SELECT name FROM added_files order by name;" echo ".output" # Deleted files are the opposite: those that are are not moved or modified # and have no 'scanned' entry but do have a 'frozen' entry. echo "CREATE VIEW deleted_files as select checksum, name from (select * from interesting_files except select * from (select checksum_frozen as checksum, name, 'frozen' as source from modified_files union select checksum_scanned as checksum, name, 'scanned' as source from modified_files union select checksum, name_frozen as name, 'frozen' as source from moved_files union select checksum, name_scanned as name, 'scanned' as source from moved_files)) where source = 'frozen';" echo ".output /tmp/$PROGNAME.$$.deleted" echo ".width 140" echo "SELECT name FROM deleted_files order by name;" echo ".output" # Deliberately hyphen in /tmp/$PROGNAME-$$.sql so rm below doesn't remove it. } > /tmp/$PROGNAME-$$.sql miniade_debug 10 "compare: running /tmp/$PROGNAME-$$.sql and not deleting it afterwards! ..." sqlite3 $DBFILE < /tmp/$PROGNAME-$$.sql # Generate the report including only those sections that have files. REPORTED_SOMETHING_FLAG=false for SUFFIX in moved modified added deleted; do if [ $(stat -c %s /tmp/$PROGNAME.$$.$SUFFIX) != 0 ]; then TITLE="$SUFFIX files" echo "$TITLE" printf "%${#TITLE}.${#TITLE}s\\n" $LOTS_OF_EQUALS echo cat /tmp/$PROGNAME.$$.$SUFFIX echo REPORTED_SOMETHING_FLAG=true fi done if $REPORTED_SOMETHING_FLAG; then echo "(To save the current state you can run '$PROGNAME --freeze $ROOT_DIR'.)" fi # Clean up. rm -f /tmp/$PROGNAME.$$.* } freeze() { local ROOT_DIR FROZEN_FILE SCANNED_FILE # Process arguments [ $# = 1 ] || miniade_bad_usage ROOT_DIR=$1 # Sanity checks and derivations [[ $ROOT_DIR =~ ^/ ]] || miniade_error "$ROOT_DIR: not absolute" FROZEN_FILE=$STATE_DIR/$(zencode <<<"$ROOT_DIR")-frozen.md5sum SCANNED_FILE=$STATE_DIR/$(zencode <<<"$ROOT_DIR")-scanned.md5sum [ -f $SCANNED_FILE ] || miniade_error "$ROOT_DIR: no scanned file to freeze" # Guts miniade_debug 10 "freeze: copying $SCANNED_FILE to $FROZEN_FILE ..." cp --preserve=timestamps $SCANNED_FILE $FROZEN_FILE } help() { local PROGNAME miniade_get_progname PROGNAME echo "Usage: $PROGNAME [ ] { --{scan|compare|freeze} [ ... ] | --list }" exit 0 } main "$@"