make source and destination directory for diff
[bak-git.git] / bak-git-server.pl
1 #!/usr/bin/perl
2
3 =head1 bak-git
4
5 Simple tracking of remote files in central git repository
6 with only shell, netcat, rsync and ssh on client
7
8 Start server, install on remote-host or upgrade with:
9
10   ./bak-git-server.pl /path/to/backup 192.168.42.42
11         [--install remote-host]
12         [--upgrade]
13
14 You will want to add following to C<~/.ssh/config>
15
16   RemoteForward 9001 localhost:9001
17
18 bak command overview:
19
20   bak add /path
21   bak commit [/path [message]]
22   bak diff [host:][/path]
23   bak status [/path]
24   bak log [/path]
25
26   bak ch[anges]
27   bak revert [host:]/path
28
29   bak cat [host:]/path
30
31   bak - push all changed files to server
32
33 See L<http://blog.rot13.org/bak-git> for more information
34
35 =cut
36
37 use warnings;
38 use strict;
39 use autodie;
40 use IO::Socket::INET;
41 use File::Path;
42 use Getopt::Long;
43
44 my $upgrade = 0;
45 my $install;
46
47 GetOptions(
48         'upgrade!'  => \$upgrade,
49         'install=s' => \$install,
50 ) || die "$!\n";
51
52 my ( $dir, $server_ip ) = @ARGV;
53 die "usage: $0 /backup/directory 127.0.0.1\n" unless $dir;
54 $server_ip ||= '127.0.0.1';
55
56 my $shell_client = <<__SHELL_CLIENT__;
57 #!/bin/sh
58 #echo \$USER/\$SUDO_USER $install `pwd` \$* | nc 127.0.0.1 9001
59 echo \$USER/\$SUDO_USER `hostname` `pwd` \$* | nc $server_ip 9001
60 __SHELL_CLIENT__
61
62 chdir $dir;
63 system 'git init' unless -e '.git';
64
65 if ( $upgrade || $install ) {
66         open(my $fh, '>', '/tmp/bak');
67         print $fh $shell_client;
68         close($fh);
69         chmod 0755, '/tmp/bak';
70
71         my @hosts = grep { -d $_ } glob '*';
72         @hosts = ( $install ) if $install;
73
74         foreach my $hostname ( @hosts ) {
75                 warn "install on $hostname\n";
76                 system 'ssh-copy-id', "root\@$hostname" if ! -d $hostname;
77                 system "scp /tmp/bak root\@$hostname:/usr/local/bin/";
78                 system "ssh root\@$hostname apt-get install -y rsync";
79         }
80 }
81
82 my $server = IO::Socket::INET->new(
83         Proto     => 'tcp',
84         LocalAddr => $server_ip,
85         LocalPort => 9001,
86         Listen    => SOMAXCONN,
87         Reuse     => 1
88 ) || die $!;
89
90
91 warn "dir: $dir listen: $server_ip:9001\n"
92         , $shell_client
93 ;
94
95 sub rsync {
96         warn "# rsync ",join(' ', @_), "\n";
97         system 'rsync', @_;
98 }
99
100 sub pull_changes {
101         my $hostname = shift;
102         system "find $hostname -type f | sed 's,$hostname,,' > /tmp/$hostname.list";
103         if ( @_ ) {
104                 open(my $files, '>>', "/tmp/$hostname.list");
105                 print $files "$_\n" foreach @_;
106                 close($files);
107         }
108         rsync split / /, "-avv --files-from /tmp/$hostname.list root\@$hostname:/ $hostname/";
109 }
110
111 while (my $client = $server->accept()) {
112         my $line = <$client>;
113         chomp($line);
114         warn "<<< $line\n";
115         my ($user,$hostname,$pwd,$command,$rel_path,$message) = split(/\s+/,$line,6);
116         $hostname =~ s/\..+$//;
117
118         my $on_host = '';
119         if ( $rel_path =~ s/^([^:]+):(.+)$/$2/ ) {
120                 if ( -e $1 ) {
121                         $on_host = $1;
122                 } else {
123                         print $client "host $1 doesn't exist in backup\n";
124                         next;
125                 }
126         }
127         my $path = $rel_path =~ m{^/} ? $rel_path : "$pwd/$rel_path";
128
129         warn "$hostname [$command] $on_host:$path | $message\n";
130
131         my $args_message = $message;
132
133         $message ||= "$path [$command]";
134         $message = "$hostname: $message";
135
136         my $dir = $path;
137         $dir =~ s{/[^/]+$}{};
138
139         my $backup_path = -e "$hostname/$path" ? "$hostname/$path" : $hostname;
140
141         sub git {
142                 my $args = join(' ',@_);
143                 warn "# git $args\n";
144                 my $out = `git $args`;
145                 warn "$out\n# [", length($out), " bytes]\n" if defined $out;
146                 return $out;
147         }
148
149         if ( ! $command ) {
150                 pull_changes $hostname;
151         } elsif ( $command eq 'add' ) {
152                 mkpath "$hostname/$dir" unless -e "$hostname/$dir";
153                 while ( $path ) {
154                         rsync( '-avv', "root\@$hostname:$path", "$hostname/$path" );
155                         print $client git 'add', "$hostname/$path";
156
157                         $args_message =~ s/^(.+)\b// || last;
158                         $path = $1;
159                         warn "? $path";
160                 }
161         } elsif ( $command eq 'commit' ) {
162                 pull_changes $hostname;
163                 $message =~ s/'/\\'/g;
164                 $user =~ s/\/$//;
165                 print $client git( "commit -m '$message' --author '$user <$hostname>' $backup_path" );
166         } elsif ( $command =~ m{(diff|status|log|ch)} ) {
167                 $command .= ' --stat' if $command eq 'log';
168                 $command = 'log --patch-with-stat' if $command =~ m/^ch/;
169                 pull_changes( $hostname ) if $command eq 'diff';
170                 if ( $on_host ) {
171                         mkpath $_ foreach grep { ! -e $_ } ( "$hostname/$dir", "$on_host/$dir" );
172                         rsync( '-avv', "root\@$hostname:$path", "$hostname/$path" );
173                         rsync( '-avv', "root\@$on_host:$path", "$on_host/$path" );
174                         open(my $diff, '-|', "diff -Nuw $hostname$path $on_host$path");
175                         while(<$diff>) {
176                                 print $client $_;
177                         }
178                 } else {
179                         # commands without path will show host-wide status/changes
180                         my $backup_path = $path ? "$hostname/$path" : "$hostname/";
181                         # hostname must end with / to prevent error from git:
182                         # ambiguous argument 'arh-hw': both revision and filename
183                         # to support branches named as hosts
184                         print $client git($command, $backup_path);
185                 }
186         } elsif ( $command eq 'revert' ) {
187                 if ( $on_host ) {
188                         rsync( '-avv', "$on_host/$path", "root\@$hostname:$path" );
189                 } else {
190                         print $client git "checkout -- $hostname/$path";
191                         rsync( '-avv', "$hostname/$path", "root\@$hostname:$path" );
192                 }
193         } elsif ( $command eq 'cat' ) {
194                 my $file_path = ( $on_host ? $on_host : $hostname ) . "/$path";
195                 open(my $file, '<', $file_path) || warn "ERROR $file_path: $!";
196                 while(<$file>) {
197                         print $client $_;
198                 }
199                 close($file);
200         } elsif ( $command eq 'ls' ) {
201                 print $client `ls $backup_path`;
202         } else {
203                 print $client "Unknown command: $command\n";
204         }
205
206 }
207