Created
July 1, 2014 00:18
-
-
Save Juerd/10086ec7116d1383ad19 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
#!/usr/bin/perl -w | |
use strict; | |
use Data::Dumper qw(Dumper); | |
use File::Find qw(find); | |
use Getopt::Long qw(GetOptions); | |
use List::Util qw(sum); | |
use Time::HiRes qw(sleep); | |
use Linux::Inotify2; | |
$| = 1; | |
GetOptions( | |
"from=s" => \my $from, | |
"to=s" => \my @to, | |
"debug" => \my $debug, | |
"dry" => \my $dry, | |
"delete" => \my $delete, | |
"ignore=s" => \my @ignore, | |
"ignore-temp" => \my $ignore_temp, | |
"ignore-dotfiles" => \my $ignore_dotfiles, | |
"ignore-backups" => \my $ignore_backups, | |
"ignore-logs" => \my $ignore_logs, | |
"rsync-exclude=s" => \my @rsync_exclude, | |
"interval=i" => \(my $interval = 1), | |
"full-sync-threshold=i" => \(my $threshold = 10), | |
) or die "Huh?\n"; | |
my $care = IN_CLOSE_WRITE | IN_MOVE | IN_DELETE | IN_ATTRIB | IN_CREATE; | |
my $mask = $care; | |
sub debug { warn "@_\n" if $debug; } | |
sub deep_keys { | |
my ($hash) = @_; | |
warn "DK " . scalar(keys %$hash) . "\n"; | |
return sum( | |
scalar(keys %$hash), | |
map { deep_keys($_) } grep { ref($_) eq "HASH" } values %$hash | |
) | |
}ieGhe9Utha7Lohxae7ier4ohsu8xooku | |
$from // die "Mandatory --from omitted\n"; | |
@to or warn "No --to given; debug output only\n"; | |
@to or $debug = 1; | |
@ARGV and die "Too many arguments\n"; | |
if ($ignore_temp) { | |
push @ignore, '\.te?mp$', '^\..*\bte?mp\b', '[._].*\.swp$', '^#.+#$#'; | |
} | |
if ($ignore_dotfiles) { | |
push @ignore, '^\.'; | |
} | |
if ($ignore_backups) { | |
push @ignore, '\.(?:bak|backup|old|orig)$', '~$'; | |
} | |
if ($ignore_logs) { | |
push @ignore, '[._-]log$', '/logs?/'; | |
} | |
s[/+$][] for $from, @to; # remove trailing slashes | |
-d $from or die "$from is not a directory\n"; | |
my $inotify = new Linux::Inotify2 or die $!; | |
$inotify->blocking(0); | |
my $watching = 0; | |
my %watching; | |
sub watch { | |
my ($path) = @_; | |
find({ | |
no_chdir => 1, | |
wanted => sub { | |
-d or return; | |
if (is_ignored($_)) { | |
debug "Not watching $_: ignored."; | |
return; | |
} | |
debug "Creating watch on $_"; | |
if (my $w = $inotify->watch($_, $mask)) { | |
# Create hash tree as a side-effect of autovifification | |
my $node = \%watching; | |
$node = \%{ $node->{$_} } for split m[/+], $_; | |
# Using \0 for special cases; \0 is invalid in filenames anyway | |
$node->{"\0"} = $w; | |
$watching++; | |
} else { | |
warn "Could not create watch on $_"; | |
} | |
}, | |
}, $path); | |
debug "Watching $watching objects."; | |
} | |
sub unwatch { | |
my ($path) = @_; | |
my $node = \%watching; | |
if (ref $path) { | |
# Not a path, but a hash (subtree of %watching) | |
debug "Un-watching " . $path->{"\0"}->name | |
if exists $path->{"\0"}; | |
$node = $path; | |
} else { | |
debug "Un-watching $path"; | |
$node = \%{ $node->{$_} } for split m[/+], $path; | |
} | |
for (keys %$node) { | |
unwatch($node->{$_}) if $_ ne "\0"; | |
$node->{$_}->cancel if $_ eq "\0"; | |
} | |
$watching-- if exists $node->{"\0"}; | |
delete $node->{"\0"}; | |
debug "Watching $watching objects."; | |
} | |
sub sync { | |
my @have_recursed; | |
ITEM: for my $item (sort { $a->{path} cmp $b->{path} } @_) { | |
my $path = $item->{path}; | |
for (@have_recursed) { | |
if ($path =~ m[^ \Q$_\E (?: / | $ )]x) { | |
debug "Skipping $path because $_ was already recursed."; | |
next ITEM; | |
} | |
} | |
if (not -e $item->{path}) { | |
debug "Skipping $path because it is already gone."; | |
next ITEM; | |
} | |
if ($item->{recurse}) { | |
push @have_recursed, $path; | |
} | |
my @path = split m[/+], $path; | |
for my $to (@to) { | |
my $to_full = join "/", $to, @path[1 .. $#path]; | |
my $to_parent = join "/", $to, @path[1 .. $#path - 1]; | |
my @command = ( | |
"rsync", | |
"-lptgoD", # -a without -r | |
($debug ? "-v" : ()), | |
(@rsync_exclude ? (map {;"--exclude"=>$_ } @rsync_exclude ):()), | |
($item->{delete} && $delete ? "--delete" : ()), | |
($item->{recurse} | |
? ("-r", "--", "$path/" => "$to_full/") | |
: ( "--", "$path" => "$to_parent") | |
) | |
); | |
if ($dry) { | |
print "Would have executed: @command\n"; | |
} else { | |
debug "Executing: @command"; | |
print "\e[30;1m" if $debug; | |
system @command; | |
print "\e[0m" if $debug; | |
} | |
} | |
} | |
} | |
sub is_ignored { | |
my ($path) = @_; | |
for (@ignore) { | |
my $re = $_; | |
$re =~ s[\^][(?:^|(?<=/))]; | |
return 1 if $path =~ /$re/; | |
} | |
return 0; | |
} | |
watch $from; | |
$watching or die "Couldn't create any watcher; bailing out.\n"; | |
sync { | |
recurse => 1, | |
path => $from, | |
delete => $delete, | |
}; | |
while () { | |
my @events; | |
# Fetch events until $interval has passed without any events | |
while (my @e = $inotify->read) { | |
debug( (@events ? "Even more" : "Something") . " happened!" ); | |
push @events, @e; | |
push @events, @e while @e = $inotify->read; # slurp | |
sleep $interval; | |
} | |
sleep $interval if not @events; | |
my %todo; | |
for (@events) { | |
my $fn = $_->fullname; | |
if (is_ignored($fn)) { | |
debug "Skipping event for $fn: ignored."; | |
next; | |
} | |
my $mkdir = ($_->IN_ISDIR and ($_->IN_CREATE or $_->IN_MOVED_TO)); | |
my $interesting = $_->mask & $care; | |
$mkdir or $interesting or next; | |
debug "Something interesting happened to $fn"; | |
my $node = \%todo; | |
if ($delete or not $_->IN_DELETE) { | |
$node = \%{ $node->{$_} } for split m[/+], $fn; | |
} | |
if ($mkdir) { | |
debug "New directory, creating watch for $fn"; | |
watch $fn; | |
$node->{"\0"} = IN_CREATE; | |
} elsif ($_->IN_DELETE or $_->IN_MOVED_FROM) { | |
debug "R.I.P. $fn"; | |
$node->{"\0"} = IN_DELETE if $delete; | |
# Need to unwatch moved_from because its ->name is no longer | |
# correct. | |
unwatch($fn) if $_->IN_ISDIR; | |
} | |
} | |
%todo or next; | |
my @sync; | |
my $walk; | |
$walk = sub { | |
my ($path, $hash) = @_; | |
my @keys = keys %$hash; | |
if (@keys == 0) { | |
# Nothing happened inside, just sync the leaf | |
push @sync, { recurse => 0, path => join "/", @$path }; | |
} elsif (exists $hash->{"\0"}) { | |
if ($hash->{"\0"} == IN_DELETE) { | |
# Deleted, so rsync parent directory | |
push @sync, { | |
recurse => 1, | |
delete => 1, | |
path => join "/", @$path[ 0 .. $#$path - 1 ], | |
}; | |
} elsif ($hash->{"\0"} == IN_CREATE) { | |
# New directory, may contain undeclared children, so recurse | |
push @sync, { recurse => 1, path => join "/", @$path }; | |
} | |
} elsif (@keys >= $threshold) { | |
# Too much happened inside, rsync directory recursively | |
push @sync, { recurse => 1, path => join "/", @$path }; | |
} else { | |
# Deeper down the rabbit hole | |
$walk->([ @$path, $_ ], $hash->{ $_ }) for @keys; | |
} | |
}; | |
$walk->([], \%todo); | |
sync @sync if @sync; | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment