Skip to content

Instantly share code, notes, and snippets.

@Juerd
Created July 1, 2014 00:18
Show Gist options
  • Save Juerd/10086ec7116d1383ad19 to your computer and use it in GitHub Desktop.
Save Juerd/10086ec7116d1383ad19 to your computer and use it in GitHub Desktop.
#!/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