3 # Continuous mirroring script around inotifywait and rsync, as suggested by
4 # Buck Huppmann. Supports local and remote pushing.
5 # EXPERIMENTAL! THERE IS ABSOLUTELY NO WARRANTY!
6 # -- Matt McCutchen <hashproduct@gmail.com>
7 # See: http://www.kepreon.com/~matt/utils/#continusync
10 # continusync path/to/srcdir/ path/to/destdir/
11 # continusync path/to/srcdir/ [user@]host:path/to/destdir/
13 # It seems to work, but it runs rsync once per event, which is ridiculous.
14 # TODO: Event batching!!!
15 # TODO: Do the recursive deletion in perl instead of calling rm(1).
20 # Configuration. TODO: Add options for these.
22 # Let the rsyncs we invoke piggyback on the main ssh connection.
23 # For this to work, you have to set a ControlPath in your ~/.ssh/config ;
24 # see the ssh_config(5) man page.
25 our @rshArgs = ('ssh', '-o', 'ControlMaster auto');
27 our $csPath = 'continusync';
29 # Don't put -r or --delete here.
31 our @rsyncArgs = ('rsync', '-lE', '--chmod=ugo=rwX', '-i');
36 # readFully(fh, length) -> data
38 my ($fh, $bytesLeft) = @_;
39 my ($buf, $off, $rv) = ('', 0);
40 while ($bytesLeft > 0) {
41 $rv = sysread($fh, $buf, $bytesLeft, $off);
42 return undef if $rv == 0; # HMMM: May lose partial read
43 die "Read error" unless $rv > 0;
50 # writeFully(fh, data)
53 my ($bytesLeft, $off, $rv) = (length($buf), 0);
54 while ($bytesLeft > 0) {
55 $rv = syswrite($fh, $buf, $bytesLeft, $off);
56 die "Write error" unless $rv > 0;
62 # readMsg(fh) -> (type, body)
65 my $head = readFully($fh, 8);
66 return (undef, undef) unless defined($head);
67 my ($type, $bodyLen) = unpack('NN', $head);
68 return ($type, readFully($fh, $bodyLen));
71 # writeMsg(fh, type, body)
73 my ($fh, $type, $body) = @_;
74 writeFully($fh, pack('NN/a*', $type, $body));
79 sub MSG_REMOTE_PATH { 1; }
80 sub MSG_PERFORMED { 2; }
83 sub MSG_DELETED { 5; }
91 while (($type, $body) = readMsg(STDIN), defined($type)) {
92 if ($type == MSG_RENAME) {
93 my ($src, $dest) = unpack('N/a*N/a*', $body);
95 writeMsg(STDOUT, MSG_PERFORMED, '');
96 } elsif ($type == MSG_DELREC) {
99 $rmPid = open($fromRm, '-|', 'rm', '-rf', '-v', $victim);
101 while (defined($rmLine = <$fromRm>)) {
103 if ($rmLine =~ /^[^`]*`(.*)'[^']*$/) {
104 writeMsg(STDOUT, MSG_DELETED, $1);
109 writeMsg(STDOUT, MSG_PERFORMED, '');
114 # The stuff below applies only to the client.
119 our ($fromServer, $toServer, $serverPid);
120 our ($fromInwt, $inwtPid);
123 print "Caught a signal. Shutting down.\n";
125 #print STDOUT "serverPid is $serverPid\n";
128 waitpid($serverPid, 0);
130 #print STDOUT "inwtPid is $inwtPid\n";
133 waitpid($inwtPid, 0);
139 my ($isRecursive, $isDelete, @paths) = @_;
141 my ($rsyncPid, $toRsync);
142 $rsyncPid = open($toRsync, '|-', @rsyncArgs,
143 ($isRecursive ? '-r' : '-d'), ($isDelete ? '--del' : ()),
144 '--no-implied-dirs', '-t', '--from0', '--files-from=-', '.', $dest);
145 foreach my $p (@paths) {
146 print $toRsync $p, "\0";
149 waitpid($rsyncPid, 0);
153 my ($src, $dest) = @_;
154 writeMsg($toServer, MSG_RENAME, pack('N/a*N/a*', $src, $dest));
155 readMsg($fromServer); # MSG_PERFORMED
156 print "*movefrom $src\n",
162 writeMsg($toServer, MSG_DELREC, $path);
164 while (($type, $body) = readMsg($fromServer), $type == MSG_DELETED) {
165 print "*deleting $body\n";
167 # Also reads the final MSG_PERFORMED.
170 # move_self so we can reliably detect moves out
171 our @interestingEvents = ('modify', 'attrib', 'move', 'move_self', 'create', 'delete');
176 print "Continusync starting up.\n",
177 "This software is EXPERIMENTAL. There is ABSOLUTELY NO WARRANTY.\n";
179 # Get a server process.
181 if ($dest =~ /^([^:]*):(.*)$/) {
182 # Invoke over remote shell
183 my ($uhost, $rdest) = ($1, $2);
184 $serverPid = open2($fromServer, $toServer, @rshArgs, $uhost, $csPath, '--server');
185 # Pass path on stdin to stop the shell from messing with it.
186 # Echoes of rsync daemon protocol...
187 writeMsg($toServer, MSG_REMOTE_PATH, $rdest);
190 my ($fromClient, $toClient);
191 pipe($fromServer, $toClient);
192 pipe($fromClient, $toServer);
194 if ($serverPid == 0) {
198 open(STDIN, "<&", $fromClient);
199 open(STDOUT, ">&", $toClient);
206 # Get a dest path that we can pass to rsync even after we chdir into the source.
209 open($localDestFH, '<', $dest);
211 $dest = "/proc/self/fd/" . fileno($localDestFH);
217 $inwtPid = open($fromInwt, '-|');
219 # Parent wants all our output on the single filehandle $fromInwt.
220 open(STDERR, ">&", STDOUT);
221 my @args = ('inotifywait', '-r', '-m', '--format', "%e\n%w\n%f", map(('-e', $_), @interestingEvents), '.');
225 <$fromInwt>; # `Setting up watches'
226 <$fromInwt>; # `Watches established'
227 $SIG{INT} = \&clientQuit;
228 print "Continuously mirroring. Give me a SIGINT when you want me to quit.\n";
230 # Now we can do the initial copy without danger of losing events.
233 # Consecutive MOVED_FROM and MOVED_TO events constitute an internal
234 # move. A move-out followed by a move-in gives an intervening
235 # MOVED_SELF, so we aren't fooled.
236 my $movedFrom = undef;
240 chomp($e = <$fromInwt>);
241 chomp($w = <$fromInwt>);
242 chomp($f = <$fromInwt>);
244 $path =~ s,^\./(.),$1,; # Remove initial ./ if it isn't all
245 my $isDir = ($e =~ s/,ISDIR$//);
246 #print "Got event: ($e,$isDir,$w,$f)\n";
248 if (defined($movedFrom)) {
249 if ($e eq 'MOVED_TO') {
251 doRename($movedFrom, $path);
255 doDelete($movedFrom);
260 if ($e eq 'MODIFY') {
261 doRsync(0, 0, $path);
262 } elsif ($e eq 'ATTRIB') {
263 doRsync(0, 0, $path);
264 } elsif ($e eq 'MOVED_FROM') {
266 } elsif ($e eq 'MOVED_TO') {
268 # Must be recursive in case it was an entire directory.
269 doRsync(1, 0, $path);
270 } elsif ($e eq 'CREATE') {
271 doRsync(0, 0, $path);
272 } elsif ($e eq 'DELETE') {
279 if ($ARGV[0] eq '--server') {
280 #STDOUT->autoflush(1);
281 my ($type, $dest) = readMsg(STDIN);
284 doClient($ARGV[0], $ARGV[1]);