#!/bin/bash # File Sync Tool for Mac 2016-02-26 for v1.10 r16 # user's log and settings p_logfile="${HOME}/Library/Logs/filesynctool_$(/bin/date +%F).log" p_plist="${HOME}/Library/Preferences/fi.aalto.filesynctool.plist" # if You edit plist manually, do check for errors with # /usr/libexec/PlistBuddy -c print ~/Library/Preferences/fi.aalto.filesynctool.plist # use Pashua for building user interface p_bin='/Applications/Utilities/Pashua.app/Contents/MacOS/Pashua' # run time GUI parameters for Pashua p_config="/private/tmp/filesynctool-$(/usr/bin/uuidgen)" # script directrory p_dir=$(/usr/bin/dirname "${0}") # system default rsync does not support -s glob security option! # use MacPorts or aaltocoreports or exit if [ -x /opt/aaltocoreports/bin/rsync ]; then p_rsync='/opt/aaltocoreports/bin/rsync' elif [ -x /opt/local/bin/rsync ]; then p_rsync='/opt/local/bin/rsync' else echo "$(/bin/date +%F_%H.%M) exit with error: no rsync with -s option?" >> "$p_logfile" exit 1 fi # .plist settings file utility and other common utitilities p_buddy='/usr/libexec/PlistBuddy' p_perl='/usr/bin/perl' p_cat='/bin/cat' # see some of those from perl, too export p_plist p_logfile p_dir p_rsync p_buddy # clean temp and do log # s_exit() { /bin/rm -f $p_config echo "${0} $(/bin/date +%F_%H.%M) exit" >> "$p_logfile" exit 0 } # after sync, show status dialog with prompt for quit, try again, view or remove log or quit # use: if error s_status("NOT ", "[reason]") # if success s_status("", "") # s_status() { args=("$@") $p_cat < $p_config # overwrite *.title = Aalto File Sync Status *.x = 40 *.y = 50 txt.type = text txt.default = Sync was ${args[0]}successful EOF # # show warning icon and error description if any if [ "NOT " == "${args[0]}" ] then msg='' for (( i=1; i<$#; i=i+1 )) do msg="$msg${args[$i]} " done $p_cat <> $p_config img.type = image img.path = ${p_dir}/_warning.png img.maxwidth = 36 error.type = text error.width = 450 error.default = $msg EOF # log warning echo "$(/bin/date +%F_%H.%M) error: $msg" >> "$p_logfile" fi # # $p_cat <> $p_config # GUI buttons, ask what next restart.type = button restart.label = Start Again log.type = button log.label = Show Log rml.type = button rml.label = Log remove quit.type = defaultbutton quit.label = Quit EOF # reveal status dialog ret=$("$p_bin" "$p_config") # act according to button pressed if [[ $ret =~ .*restart=1.* ]] then return # back to main loop & main GUI elif [[ $ret =~ .*log=1.* ]] then /usr/bin/open -a /Applications/TextEdit.app "$p_logfile" elif [[ $ret =~ .*rml=1.* ]] then rm "$(dirname $p_logfile)/filesynctool_"* fi s_exit } # error happened, do warn and log # s_err() { s_status 'NOT ' 'Error' "$@" } # build upper part of settings dialog GUI # s_gui_top() { $p_cat << EOF > $p_config # overwrite! *.title = Aalto File Sync Tool *.x = 40 *.y = 50 EOF } # build lower part of settings dialog GUI with buttons # s_gui_bottom() { $p_cat <> $p_config help.type = button help.label = Help cancel.type = button cancel.label = Quit run.type = defaultbutton run.label = Start Sync EOF } # read sync settings from settings file to hash of # hashes and build GUI. To cure syntax headache, see # perldsc(1), perlretut(1) and PlistBuddy(8) # s_gui() { s_gui_top $p_buddy -c print $p_plist | $p_perl -we ' use strict; my (@row, %hash, $syncpair); while(<>) { chomp; @row = split; if ( /^ {4}[^ }]/ ) { # syncpair name $syncpair = $row[0]; } if ( /^ {8}/ ) { # option key and value /^.*= (.*?)$/; # save value if any $hash{$syncpair}{$row[0]} = $1 ? $1 : ""; } } # get hash references to hashes with source, target & delete values for $syncpair ( sort keys %hash ) { # avoid uninitialized values my ($src, $dst); $src = $hash{$syncpair}{"source"} ? $hash{$syncpair}{"source"} : ""; $dst = $hash{$syncpair}{"destination"} ? $hash{$syncpair}{"destination"} : ""; # dump hash to Pashua GUI format print <> $p_config s_gui_bottom } # no settings available, use empty defaults for GUI # s_gui_default() { s_gui_top for i in "1st" "2nd" "3rd" do $p_cat <> $p_config s_gui_bottom } # main loop # while : do if [ -e $p_plist ] # set up GUI then s_gui # with saved values else s_gui_default # or without fi # reveal GUI, then process user input echo -e "\n\n$(/bin/date +%F_%H.%M) starting GUI for settings" >> "$p_logfile" $p_bin $p_config | $p_perl -we ' use strict; use POSIX qw(strftime); # hashes for transpose table and sync pair settings my (%t, %h); # initialize with emptiness %h = (); # log when needed sub _synclog { open my $logfile, ">>", "$ENV{p_logfile}"; print $logfile strftime("+ %F_%H.%M ", localtime), @_, "\n"; close $logfile; } # free space in kB sub _dfree { my ($fh, @df); open $fh, "-|", "/bin/df", "-k", @_; @df=split while <$fh>; close $fh; return $df[3] } # disk usage in kB sub _dusage { my ($fh, @du); open $fh, "-|", "/usr/bin/du", "-sk", @_; @du=split while <$fh>; close $fh; return $du[0] } # read sync settings from user input to hash of hashes, see perldsc(1) while(<>) { chomp; exit 7 if /^cancel=1$/; exit 8 if /^help=1$/; next if /^help=0$/ or /^cancel=0$/ or /^run=0$/ or /^run=1$/ or /[st]=$/; my @row = split /=/; $row[0] =~ /^(.*?)(.)$/; # save name, action $h{$1}{$2} = $row[1] ? $row[1] : ""; # and path } # transpose to PlistBuddy commands %t = ( e => "enabled integer", d => "delete integer", t => "destination string", s => "source string" ); # forget old... rename "$ENV{p_plist}", "$ENV{p_plist}.bak"; # ...and save new settings for my $p ( sort keys %h ) { for my $k ( sort keys %{ $h{$p} } ) { system( $ENV{p_buddy}, "-c", "add :$p:$t{$k} $h{$p}{$k}", $ENV{p_plist} ); } } # if mishap with saving, try the back up unless ( -e $ENV{p_plist} ) { _synclog "warn: sync settings file lost, trying the restore the back up"; rename "$ENV{p_plist}.bak", $ENV{p_plist} if -s "$ENV{p_plist}.bak"; } # variables my ( $source, $target, $enabled, $delete, @args, $retval, $pair, $k, $count ); $count=0; # every source - destination sync pair for $pair ( sort keys %h ) { # emptify $source=$target=$enabled=$delete=""; @args=(); # sync pair PROPERTIES: for $k ( sort keys %{ $h{$pair} } ) { # [x] enabled ? if ( "e" eq $k && ! ( "1" eq $h{$pair}{"e"} ) ) { $enabled="FALSE"; _synclog "skipping sync ${pair}: not enabled by GUI checkbox"; last PROPERTIES; } # [x] delete ? if ( "d" eq $k ) { if ( defined($h{$pair}{"d"}) && ("1" eq $h{$pair}{"d"}) ) { $delete="--del" } else { $delete="--" } next; } # save path, or skip if empty if ( "" ne $h{$pair}{$k} ) { $source="$h{$pair}{$k}" if "s" eq $k; $target="$h{$pair}{$k}" if "t" eq $k; } } # sync pair disabled in GUI? next if "FALSE" eq $enabled; # to prevent backups recursively inside itself, source # and target must not be the same next if $source eq $target; # rsync works differently with "path" or "path/" as source # this time we stick with "path" behavior in app and docs # as side efect do unsupport / as source! $source=~s,/$,,g; # paths should be useable, too exit 3 unless -d "$source" and -r "$source" and -d "$target" and -w "$target"; # does the sync fit into? # # abort if tight on free disk space on target # ( _dfree($target) < _dusage($source) ) && exit 15; # # just warn -- we do not know if there are just a few changed files to update if ( _dfree("$target") < _dusage("$source") ) { _synclog join " ", $pair, ":", $source, ">", $target, "warning: source disk usage bigger than target free space"; } # sync settings @args = ( ${ENV{p_rsync}}, "--log-file=${ENV{p_logfile}}", "-a", "-s", "-v", "$delete", "$source", "$target" ); # log start attempt _synclog join " ", "sync", $pair, "starting with\n", @args; # run $retval=system @args; # if error abort syncs exit $retval >> 8 if 0 != $retval; # log success _synclog join " ", "sync", $pair, "run", ++$count, "without error"; } exit 9 if 0 == $count; ' case $? in 0) s_status '' '' # all syncs without error ;; 7) s_exit # quit button pressed ;; 8) /usr/bin/open "${p_dir}/_AaltoFileSyncToolHelp.html" # help button pressed s_exit ;; 9) s_err "#${?} Check sources, targets and enabled checkboxes: Nothing to sync, paths non-existent or not folders, or same path as source and target" ;; 15) s_err "#${?} Not enough free space for the sync on target volume" ;; 1) s_err "#${?} Syntax or usage error" ;; 2) s_err "#${?} Protocol incompatibility" ;; 3) s_err "#${?} selecting input/output files, dirs" ;; 4) s_err "#${?}: Requested action not supported: an attempt was made to manipulate 64-bit files on a platform that cannot support them; or an option was specified that is supported by the client and not by the server" ;; 5) s_err "#${?} starting client-server protocol" ;; 6) s_err "#${?} Daemon unable to append to log-file" ;; 10) s_err "#${?} in socket I/O" ;; 11) s_err "#${?} in file I/O" ;; 12) s_err "#${?} in rsync protocol data stream" ;; 13) s_err "#${?} with program diagnostics" ;; 14) s_err "#${?} in IPC code" ;; 20) s_err "#${?} Received SIGUSR1 or SIGINT" ;; 21) s_err "#${?} Some error returned by waitpid()" ;; 22) s_err "#${?} allocating core memory buffers" ;; 23) s_err "#${?} Partial transfer due to error" ;; 24) s_err "#${?} Partial transfer due to vanished source files" ;; 25) s_err "#${?} The --max-delete limit stopped deletions" ;; 30) s_err "#${?} Timeout in data send/receive" ;; 35) s_err "#${?} Timeout waiting for daemon connection" ;; *) s_err "#${?} Unknown error" ;; esac /bin/sleep 1; done ## More features? Start a project maybe? See also: # https://www.cis.upenn.edu/~bcpierce/unison/ # http://www.opbyte.it/grsync/ # https://rsnapshot.org # http://www.mikerubel.org/computers/rsync_snapshots/ # http://shop.oreilly.com/product/9780596102463.do # https://fedoramagazine.org/butterfly-backup/ # https://dropbox.tech/infrastructure/rewriting-the-heart-of-our-sync-engine