/[psinib]/psinib.pl
This is repository of my old source code which isn't updated any more. Go to git.rot13.org for current projects!
ViewVC logotype

Annotation of /psinib.pl

Parent Directory Parent Directory | Revision Log Revision Log


Revision 1.22 - (hide annotations)
Mon Mar 1 19:38:16 2004 UTC (20 years, 1 month ago) by dpavlin
Branch: MAIN
CVS Tags: HEAD
Changes since 1.21: +6 -3 lines
File MIME type: text/plain
if home directory isn't writable (for e.g. if you are running psinib under
backup user with Debian) it will fallback to /tmp directory (which is
somewhat bad -- why whould you want to run under user without writable home
dir?)

1 dpavlin 1.1 #!/usr/bin/perl -w
2     #
3     # psinib - Perl Snapshot Is Not Incremental Backup
4     #
5     # written by Dobrica Pavlinusic <dpavlin@rot13.org> 2003-01-03
6     # released under GPL v2 or later.
7     #
8     # Backup SMB directories using file produced by LinNeighbourhood (or some
9     # other program [vi :-)] which produces file in format:
10     #
11     # smbmount service mountpoint options
12     #
13     #
14     # usage:
15 dpavlin 1.4 # $ psinib.pl mountscript
16 dpavlin 1.1
17     use strict 'vars';
18     use Data::Dumper;
19     use Net::Ping;
20     use POSIX qw(strftime);
21     use List::Compare;
22     use Filesys::SmbClient;
23     #use Taint;
24 dpavlin 1.2 use Fcntl qw(LOCK_EX LOCK_NB);
25 dpavlin 1.4 use Digest::MD5;
26     use File::Basename;
27 dpavlin 1.14 use Getopt::Long;
28 dpavlin 1.1
29     # configuration
30     my $LOG_TIME_FMT = '%Y-%m-%d %H:%M:%S'; # strftime format for logfile
31     my $DIR_TIME_FMT = '%Y%m%d'; # strftime format for backup dir
32    
33 dpavlin 1.16 # define timeout for ping
34     my $PING_TIMEOUT = 5;
35    
36 dpavlin 1.1 my $LOG = '/var/log/backup.log'; # add path here...
37 dpavlin 1.5 #$LOG = '/tmp/backup.log';
38 dpavlin 1.1
39     # store backups in which directory
40 dpavlin 1.13 my $BACKUP_DEST = '/backup/isis_backup';
41     #my $BACKUP_DEST = '/tmp/backup/';
42 dpavlin 1.1
43     # files to ignore in backup
44     my @ignore = ('.md5sum', '.backupignore', 'backupignore.txt');
45    
46     # open log
47 dpavlin 1.5 open(L, ">> $LOG") || die "can't open log $LOG: $!";
48 dpavlin 1.1 select((select(L), $|=1)[0]); # flush output
49 dpavlin 1.2
50     # make a lock on logfile
51    
52     my $c = 0;
53     {
54     flock L, LOCK_EX | LOCK_NB and last;
55     sleep 1;
56     redo if ++$c < 10;
57     # no response for 10 sec, bail out
58 dpavlin 1.12 xlog("ABORT","can't take lock on $LOG -- another $0 running?");
59 dpavlin 1.2 exit 1;
60     }
61 dpavlin 1.1
62     # taint path: nmblookup should be there!
63     $ENV{'PATH'} = "/usr/bin:/bin";
64    
65 dpavlin 1.17 my $use_ping = 1; # default: use syn tcp ping to verify that host is up
66     my $verbose = 1; # default verbosity level
67     my $quiet = 0;
68 dpavlin 1.20 my $email;
69 dpavlin 1.14
70     my $result = GetOptions(
71     "ping!" => \$use_ping, "backupdest!" => \$BACKUP_DEST,
72 dpavlin 1.17 "verbose+" => \$verbose, "quiet+" => \$quiet,
73 dpavlin 1.20 "email=s" => \$email,
74 dpavlin 1.14 );
75    
76 dpavlin 1.17 $verbose -= $quiet;
77    
78 dpavlin 1.1 my $mounts = shift @ARGV ||
79     'mountscript';
80     # die "usage: $0 mountscript";
81    
82 dpavlin 1.20 my $basedir = $0;
83     $basedir =~ s,/?[^/]+$,,g;
84    
85     # default subject for e-mail messages
86     my @subjects = ('Backup needs your attention!');
87     my $sub_nr = 0;
88     my $email_body;
89    
90 dpavlin 1.22 my $home_dir=$ENV{'HOME'};
91     $home_dir = '/tmp' if (! -w $home_dir);
92    
93 dpavlin 1.20 if ($email) {
94     # It will use (and require) Tie::File only if --email=foo@bar.com
95     # arguement is used!
96     use Tie::File;
97     tie @subjects, 'Tie::File', "$basedir/subjects.txt" || xlog("CONFIG","Can't find $basedir/subjects.txt... using default (only one)");
98     chdir; # this will change directory to HOME
99 dpavlin 1.22 if (open(SN,"$home_dir/.psinib.subject")) {
100 dpavlin 1.20 $sub_nr = <SN>;
101     chomp($sub_nr);
102     close(SN);
103     }
104     $sub_nr++;
105     # skip comments in subjects.txt
106 dpavlin 1.21 while($subjects[$sub_nr] && $subjects[$sub_nr] =~ m/^#/) {
107 dpavlin 1.20 $sub_nr++;
108     }
109     $sub_nr = 0 if (! $subjects[$sub_nr]);
110    
111 dpavlin 1.22 if (open(SN,"> $home_dir/.psinib.subject")) {
112 dpavlin 1.20 print SN "$sub_nr\n";
113     close (SN);
114     } else {
115 dpavlin 1.22 xlog("CONFIG","Can't open $home_dir/.psinib.subject -- I can't cycle subjects...");
116 dpavlin 1.20 };
117     }
118 dpavlin 1.1
119     my @in_backup; # shares which are backeduped this run
120    
121 dpavlin 1.16 # init Net::Ping object
122 dpavlin 1.14 my $ping;
123     if ($use_ping) {
124 dpavlin 1.16 $ping = new Net::Ping->new("syn", 2);
125 dpavlin 1.14 # ping will try tcp connect to netbios-ssn (139)
126     $ping->{port_num} = getservbyname("netbios-ssn", "tcp");
127     }
128 dpavlin 1.1
129 dpavlin 1.16 # do syn ping to cifs port
130     sub host_up {
131     my $ping = shift || return;
132     my $host_ip = shift || xlog("host_up didn't get IP");
133     my $timeout = shift;
134     return 1 if (! $use_ping);
135    
136     $ping->ping($host_ip,$timeout);
137     my $return = 0;
138    
139     while (my ($host,$rtt,$ip) = $ping->ack) {
140     xlog("","HOST: $host [$ip] ACKed in $rtt seconds");
141     $return = 1 if ($ip eq $host_ip);
142     }
143     return $return;
144     }
145    
146 dpavlin 1.1 my $backup_ok = 0;
147    
148     my $smb;
149     my %smb_atime;
150     my %smb_mtime;
151 dpavlin 1.4 my %file_md5;
152 dpavlin 1.1
153     open(M, $mounts) || die "can't open $mounts: $!";
154     while(<M>) {
155     chomp;
156     next if !/^\s*smbmount\s/;
157     my (undef,$share,undef,$opt) = split(/\s+/,$_,4);
158    
159 dpavlin 1.11 my ($user,$passwd,$workgroup,$ip);
160 dpavlin 1.1
161     foreach (split(/,/,$opt)) {
162     my ($n,$v) = split(/=/,$_,2);
163     if ($n =~ m/username/i) {
164     if ($v =~ m#^(.+)/(.+)%(.+)$#) {
165     ($user,$passwd,$workgroup) = ($1,$2,$3);
166     } elsif ($v =~ m#^(.+)/(.+)$#) {
167     ($user,$workgroup) = ($1,$2);
168     } elsif ($v =~ m#^(.+)%(.+)$#) {
169     ($user,$passwd) = ($1,$2);
170     } else {
171     $user = $v;
172     }
173     } elsif ($n =~ m#workgroup#i) {
174     $workgroup = $v;
175 dpavlin 1.11 } elsif ($n =~ m#ip#i) {
176     $ip = $v;
177 dpavlin 1.1 }
178     }
179    
180     push @in_backup,$share;
181    
182 dpavlin 1.4
183     my ($host,$dir,$date_dir) = share2host_dir($share);
184     my $bl = "$BACKUP_DEST/$host/$dir/latest"; # latest backup
185     my $bc = "$BACKUP_DEST/$host/$dir/$date_dir"; # current one
186     my $real_bl;
187 dpavlin 1.9 if (-l $bl) {
188 dpavlin 1.4 $real_bl=readlink($bl) || die "can't read link $bl: $!";
189     $real_bl="$BACKUP_DEST/$host/$dir/$real_bl" if (substr($real_bl,0,1) ne "/");
190 dpavlin 1.9 if (-l $bc && $real_bl eq $bc) {
191 dpavlin 1.17 xlog($share,"allready backuped...");
192 dpavlin 1.4 $backup_ok++;
193     next;
194     }
195    
196     }
197    
198    
199 dpavlin 1.17 xlog($share,"working on $share...");
200 dpavlin 1.1
201 dpavlin 1.11 # try to nmblookup IP
202     $ip = get_ip($share) if (! $ip);
203 dpavlin 1.1
204     if ($ip) {
205     xlog($share,"IP is $ip");
206 dpavlin 1.16 if (host_up($ping, $ip,$PING_TIMEOUT)) {
207 dpavlin 1.12 if (snap_share($share,$user,$passwd,$workgroup)) {
208     $backup_ok++;
209     }
210 dpavlin 1.1 }
211     }
212     }
213     close(M);
214    
215 dpavlin 1.20 my $total = ($#in_backup + 1) || 0;
216     my $pcnt = "";
217     $pcnt = "(".int($backup_ok*100/$total)." %)" if ($total > 0);
218     xlog("","$backup_ok backups completed of total $total this time".$pcnt);
219    
220     send_email();
221 dpavlin 1.1
222     1;
223    
224     #-------------------------------------------------------------------------
225    
226 dpavlin 1.4
227 dpavlin 1.1 # get IP number from share
228     sub get_ip {
229     my $share = shift;
230    
231     my $host = $1 if ($share =~ m#//([^/]+)/#);
232    
233     my $ip = `nmblookup $host`;
234     if ($ip =~ m/(\d+\.\d+\.\d+\.\d+)\s$host/i) {
235     return $1;
236     }
237     }
238    
239 dpavlin 1.20 # send e-mail with all messages
240     sub send_email {
241     return if (! $email || $email eq "" || !$email_body);
242     require Mail::Send;
243     my $msg = new Mail::Send;
244     $msg->to($email);
245     $msg->subject($subjects[$sub_nr]);
246     my $fn=$msg->open;
247     print $fn $email_body;
248     $fn->close;
249     }
250    
251 dpavlin 1.4
252     # write entry to screen and log
253 dpavlin 1.1 sub xlog {
254     my $share = shift;
255     my $t = strftime $LOG_TIME_FMT, localtime;
256     my $m = shift || '[no log entry]';
257 dpavlin 1.18 my $l = shift;
258     $l = 1 if (! defined $l); # default verbosity is 1
259 dpavlin 1.20 if ($verbose >= $l) {
260     if (! $email) {
261     print STDERR $m,"\n";
262     # don't e-mail mesages with verbosity < 1
263     } elsif ($l < 1) {
264     $email_body .= $m."\n";
265     }
266     }
267 dpavlin 1.1 print L "$t $share\t$m\n";
268     }
269    
270 dpavlin 1.7 # dump warn and dies into log
271 dpavlin 1.20 BEGIN { $SIG{'__WARN__'} = sub { xlog('WARN',$_[0],1) ; exit 1 } }
272     BEGIN { $SIG{'__DIE__'} = sub { xlog('DIE',$_[0],0) ; exit 1 } }
273 dpavlin 1.7
274 dpavlin 1.1
275 dpavlin 1.4 # split share name to host, dir and currnet date dir
276     sub share2host_dir {
277 dpavlin 1.1 my $share = shift;
278     my ($host,$dir);
279     if ($share =~ m#//([^/]+)/(.+)$#) {
280     ($host,$dir) = ($1,$2);
281     $dir =~ s/\W/_/g;
282     $dir =~ s/^_+//;
283     $dir =~ s/_+$//;
284     } else {
285 dpavlin 1.17 xlog($share,"Can't parse share $share into host and directory!",1);
286 dpavlin 1.1 return;
287     }
288 dpavlin 1.4 return ($host,$dir,strftime $DIR_TIME_FMT, localtime);
289     }
290    
291 dpavlin 1.1
292 dpavlin 1.4 # make a snapshot of a share
293     sub snap_share {
294    
295     my $share = shift;
296    
297     my %param = ( debug => 0 );
298    
299 dpavlin 1.8 $param{username} = shift || warn "can't find username for share $share";
300     $param{password} = shift || warn "can't find passwod for share $share";
301     $param{workgroup} = shift || warn "can't find workgroup for share $share";
302 dpavlin 1.4
303     my ($host,$dir,$date_dir) = share2host_dir($share);
304 dpavlin 1.1
305     # latest backup directory
306     my $bl = "$BACKUP_DEST/$host/$dir/latest";
307     # current backup directory
308     my $bc = "$BACKUP_DEST/$host/$dir/$date_dir";
309    
310     my $real_bl;
311 dpavlin 1.9 if (-l $bl) {
312 dpavlin 1.1 $real_bl=readlink($bl) || die "can't read link $bl: $!";
313     $real_bl="$BACKUP_DEST/$host/$dir/$real_bl" if (substr($real_bl,0,1) ne "/");
314 dpavlin 1.19 if (! -e $real_bl) {
315     xlog($share,"latest link $bl -> $real_bl not valid, removing it");
316     unlink $bl || die "can't remove link $bl: $!";
317     undef $real_bl;
318     }
319 dpavlin 1.18 }
320     if (! $real_bl) {
321 dpavlin 1.19 xlog($share,"no old backup, trying to find last backup");
322 dpavlin 1.7 if (opendir(BL_DIR, "$BACKUP_DEST/$host/$dir")) {
323     my @bl_dirs = sort grep { !/^\./ && -d "$BACKUP_DEST/$host/$dir/$_" } readdir(BL_DIR);
324     closedir(BL_DIR);
325     $real_bl=pop @bl_dirs;
326 dpavlin 1.17 xlog($share,"using $real_bl as latest...");
327 dpavlin 1.7 $real_bl="$BACKUP_DEST/$host/$dir/$real_bl" if (substr($real_bl,0,1) ne "/");
328     if ($real_bl eq $bc) {
329     xlog($share,"latest from today (possible partial backup)");
330     rename $real_bl,$real_bl.".partial" || warn "can't reaname partial backup: $!";
331     $real_bl .= ".partial";
332     }
333     } else {
334 dpavlin 1.17 xlog($share,"this is first run...");
335 dpavlin 1.7 }
336 dpavlin 1.1 }
337    
338 dpavlin 1.9 if (-l $bc && $real_bl && $real_bl eq $bc) {
339 dpavlin 1.17 xlog($share,"allready backuped...");
340 dpavlin 1.12 return 1;
341 dpavlin 1.1 }
342    
343     die "You should really create BACKUP_DEST [$BACKUP_DEST] by hand! " if (!-e $BACKUP_DEST);
344    
345     if (! -e "$BACKUP_DEST/$host") {
346     mkdir "$BACKUP_DEST/$host" || die "can't make dir for host $host, $BACKUP_DEST/$host: $!";
347 dpavlin 1.17 xlog($share,"created host directory $BACKUP_DEST/$host...");
348 dpavlin 1.1 }
349    
350     if (! -e "$BACKUP_DEST/$host/$dir") {
351     mkdir "$BACKUP_DEST/$host/$dir" || die "can't make dir for share $share, $BACKUP_DEST/$host/$dir $!";
352 dpavlin 1.17 xlog($share,"created dir for this share $BACKUP_DEST/$host/$dir...");
353 dpavlin 1.1 }
354    
355     mkdir $bc || die "can't make dir for current backup $bc: $!";
356    
357     my @dirs = ( "/" );
358     my @smb_dirs = ( "/" );
359    
360     my $transfer = 0; # bytes transfered over network
361    
362     # this will store all available files and sizes
363     my @files;
364     my %file_size;
365     my %file_atime;
366     my %file_mtime;
367 dpavlin 1.4 #my %file_md5;
368 dpavlin 1.13 %file_md5 = ();
369 dpavlin 1.1
370     my @smb_files;
371     my %smb_size;
372     #my %smb_atime;
373     #my %smb_mtime;
374    
375     sub norm_dir {
376     my $foo = shift;
377     my $prefix = shift;
378     $foo =~ s#//+#/#g;
379     $foo =~ s#/+$##g;
380     $foo =~ s#^/+##g;
381     return $prefix.$foo if ($prefix);
382     return $foo;
383     }
384    
385     # read local filesystem
386     my $di = 0;
387     while ($di <= $#dirs && $real_bl) {
388     my $d=$dirs[$di++];
389 dpavlin 1.7 opendir(DIR,"$real_bl/$d") || warn "opendir($real_bl/$d): $!\n";
390 dpavlin 1.1
391     # read .backupignore if exists
392 dpavlin 1.7 if (-f "$real_bl/$d/.backupignore") {
393     open(I,"$real_bl/$d/.backupignore");
394 dpavlin 1.1 while(<I>) {
395     chomp;
396     push @ignore,norm_dir("$d/$_");
397     }
398     close(I);
399 dpavlin 1.9 #print STDERR "ignore: ",join("|",@ignore),"\n";
400 dpavlin 1.7 link "$real_bl/$d/.backupignore","$bc/$d/.backupignore" ||
401     warn "can't copy $real_bl/$d/.backupignore to current backup dir: $!\n";
402 dpavlin 1.1 }
403    
404     # read .md5sum if exists
405 dpavlin 1.7 if (-f "$real_bl/$d/.md5sum") {
406     open(I,"$real_bl/$d/.md5sum");
407 dpavlin 1.1 while(<I>) {
408     chomp;
409     my ($md5,$f) = split(/\s+/,$_,2);
410     $file_md5{$f}=$md5;
411     }
412     close(I);
413     }
414    
415     my @clutter = readdir(DIR);
416     foreach my $f (@clutter) {
417     next if ($f eq '.');
418     next if ($f eq '..');
419     my $pr = norm_dir("$d/$f"); # path relative
420 dpavlin 1.7 my $pf = norm_dir("$d/$f","$real_bl/"); # path full
421 dpavlin 1.1 if (grep(/^\Q$pr\E$/,@ignore) == 0) {
422     if (-f $pf) {
423     push @files,$pr;
424     $file_size{$pr}=(stat($pf))[7];
425     $file_atime{$pr}=(stat($pf))[8];
426     $file_mtime{$pr}=(stat($pf))[9];
427     } elsif (-d $pf) {
428     push @dirs,$pr;
429     } else {
430 dpavlin 1.17 xlog($share,"not file or directory: $pf",0);
431 dpavlin 1.1 }
432     } else {
433 dpavlin 1.17 xlog($share,"ignored: $pr");
434 dpavlin 1.1 }
435     }
436     }
437    
438 dpavlin 1.12 # local dir always include /
439     xlog($share,($#files+1)." files and ".($#dirs)." dirs on local disk before backup");
440 dpavlin 1.1
441     # read smb filesystem
442    
443     xlog($share,"smb to $share as $param{username}/$param{workgroup}");
444    
445     # FIX: how to aviod creation of ~/.smb/smb.conf ?
446     $smb = new Filesys::SmbClient(%param) || die "SmbClient :$!\n";
447    
448     $di = 0;
449     while ($di <= $#smb_dirs) {
450 dpavlin 1.9 my $d=$smb_dirs[$di];
451 dpavlin 1.1 my $pf = norm_dir($d,"smb:$share/"); # path full
452 dpavlin 1.9 my $D = $smb->opendir($pf);
453     if (! $D) {
454 dpavlin 1.18 xlog($share,"FATAL: $share [$pf] as $param{username}/$param{workgroup}: $!",0);
455 dpavlin 1.9 # remove failing dir
456     delete $smb_dirs[$di];
457 dpavlin 1.12 return 0; # failed
458 dpavlin 1.9 }
459     $di++;
460 dpavlin 1.1
461     my @clutter = $smb->readdir_struct($D);
462     foreach my $item (@clutter) {
463     my $f = $item->[1];
464     next if ($f eq '.');
465     next if ($f eq '..');
466     my $pr = norm_dir("$d/$f"); # path relative
467     my $pf = norm_dir("$d/$f","smb:$share/"); # path full
468     if (grep(/^\Q$pr\E$/,@ignore) == 0) {
469     if ($item->[0] == main::SMBC_FILE) {
470     push @smb_files,$pr;
471     $smb_size{$pr}=($smb->stat($pf))[7];
472     $smb_atime{$pr}=($smb->stat($pf))[10];
473     $smb_mtime{$pr}=($smb->stat($pf))[11];
474     } elsif ($item->[0] == main::SMBC_DIR) {
475     push @smb_dirs,$pr;
476     } else {
477 dpavlin 1.17 xlog($share,"not file or directory [".$item->[0]."]: $pf",0);
478 dpavlin 1.1 }
479     } else {
480 dpavlin 1.17 xlog($share,"smb ignored: $pr");
481 dpavlin 1.1 }
482     }
483     }
484    
485 dpavlin 1.12 xlog($share,($#smb_files+1)." files and ".($#smb_dirs)." dirs on remote share");
486 dpavlin 1.1
487     # sync dirs
488     my $lc = List::Compare->new(\@dirs, \@smb_dirs);
489    
490     my @dirs2erase = $lc->get_Lonly;
491     my @dirs2create = $lc->get_Ronly;
492     xlog($share,($#dirs2erase+1)." dirs to erase and ".($#dirs2create+1)." dirs to create");
493    
494     # create new dirs
495     foreach (sort @smb_dirs) {
496     mkdir "$bc/$_" || warn "mkdir $_: $!\n";
497     }
498    
499     # sync files
500     $lc = List::Compare->new(\@files, \@smb_files);
501    
502     my @files2erase = $lc->get_Lonly;
503     my @files2create = $lc->get_Ronly;
504     xlog($share,($#files2erase+1)." files to erase and ".($#files2create+1)." files to create");
505    
506     sub smb_copy {
507     my $smb = shift;
508    
509     my $from = shift;
510     my $to = shift;
511    
512    
513     my $l = 0;
514    
515     foreach my $f (@_) {
516     #print "smb_copy $from/$f -> $to/$f\n";
517 dpavlin 1.4 my $md5 = Digest::MD5->new;
518    
519 dpavlin 1.1 my $fd = $smb->open("$from/$f");
520     if (! $fd) {
521 dpavlin 1.12 xlog("WARNING","can't open smb file $from/$f: $!");
522     next;
523     }
524    
525     if (! open(F,"> $to/$f")) {
526     xlog("WARNING","can't open new file $to/$f: $!");
527 dpavlin 1.1 next;
528     }
529    
530     while (defined(my $b=$smb->read($fd,4096))) {
531     print F $b;
532     $l += length($b);
533 dpavlin 1.4 $md5->add($b);
534 dpavlin 1.1 }
535    
536     $smb->close($fd);
537     close(F);
538    
539 dpavlin 1.4 $file_md5{$f} = $md5->hexdigest;
540    
541 dpavlin 1.1 # FIX: this fails with -T
542     my ($a,$m) = ($smb->stat("$from/$f"))[10,11];
543     utime $a, $m, "$to/$f" ||
544     warn "can't update utime on $to/$f: $!\n";
545    
546     }
547     return $l;
548     }
549    
550     # copy new files
551     foreach (@files2create) {
552     $transfer += smb_copy($smb,"smb:$share",$bc,$_);
553     }
554    
555     my $size_sync = 0;
556     my $atime_sync = 0;
557     my $mtime_sync = 0;
558     my @sync_files;
559     my @ln_files;
560    
561     foreach ($lc->get_intersection) {
562    
563     my $f;
564    
565     if ($file_size{$_} != $smb_size{$_}) {
566     $f=$_;
567     $size_sync++;
568     }
569     if ($file_atime{$_} != $smb_atime{$_}) {
570     $f=$_;
571     $atime_sync++;
572     }
573     if ($file_mtime{$_} != $smb_mtime{$_}) {
574     $f=$_;
575     $mtime_sync++;
576     }
577    
578     if ($f) {
579     push @sync_files, $f;
580     } else {
581     push @ln_files, $_;
582     }
583     }
584    
585     xlog($share,($#sync_files+1)." files will be updated (diff: $size_sync size, $atime_sync atime, $mtime_sync mtime), ".($#ln_files+1)." will be linked.");
586    
587     foreach (@sync_files) {
588     $transfer += smb_copy($smb,"smb:$share",$bc,$_);
589     }
590    
591     xlog($share,"$transfer bytes transfered...");
592    
593     foreach (@ln_files) {
594 dpavlin 1.7 link "$real_bl/$_","$bc/$_" || warn "link $real_bl/$_ -> $bc/$_: $!\n";
595 dpavlin 1.1 }
596    
597     # remove files
598     foreach (sort @files2erase) {
599     unlink "$bc/$_" || warn "unlink $_: $!\n";
600 dpavlin 1.13 delete $file_md5{$_};
601 dpavlin 1.1 }
602    
603     # remove not needed dirs (after files)
604     foreach (sort @dirs2erase) {
605     rmdir "$bc/$_" || warn "rmdir $_: $!\n";
606     }
607    
608 dpavlin 1.4 # remove old .md5sum
609     foreach (sort @dirs) {
610     unlink "$bc/$_/.md5sum" if (-e "$bc/$_/.md5sum");
611     }
612    
613 dpavlin 1.13 # erase stale entries in .md5sum
614     my @md5_files = keys %file_md5;
615     $lc = List::Compare->new(\@md5_files, \@smb_files);
616     foreach my $file ($lc->get_Lonly) {
617     xlog("NOTICE","removing stale '$file' from .md5sum");
618     delete $file_md5{$file};
619     }
620    
621 dpavlin 1.4 # create .md5sum
622     my $last_dir = '';
623     my $md5;
624 dpavlin 1.7 foreach my $f (sort { $file_md5{$a} cmp $file_md5{$b} } keys %file_md5) {
625 dpavlin 1.4 my $dir = dirname($f);
626     my $file = basename($f);
627 dpavlin 1.10 #print "$f -- $dir / $file<--\n";
628 dpavlin 1.4 if ($dir ne $last_dir) {
629     close($md5) if ($md5);
630     open($md5, ">> $bc/$dir/.md5sum") || warn "can't create $bc/$dir/.md5sum: $!";
631     $last_dir = $dir;
632 dpavlin 1.7 #print STDERR "writing $last_dir/.md5sum\n";
633 dpavlin 1.4 }
634     print $md5 $file_md5{$f}," $file\n";
635     }
636 dpavlin 1.11 close($md5) if ($md5);
637 dpavlin 1.1
638     # create leatest link
639 dpavlin 1.7 #print "ln -s $bc $real_bl\n";
640 dpavlin 1.9 if (-l $bl) {
641 dpavlin 1.7 unlink $bl || warn "can't remove old latest symlink $bl: $!\n";
642     }
643     symlink $bc,$bl || warn "can't create latest symlink $bl -> $bc: $!\n";
644    
645     # FIX: sanity check -- remove for speedup
646 dpavlin 1.9 xlog($share,"failed to create latest symlink $bl -> $bc...") if (readlink($bl) ne $bc || ! -l $bl);
647 dpavlin 1.1
648     xlog($share,"backup completed...");
649 dpavlin 1.12
650     return 1;
651 dpavlin 1.1 }
652 dpavlin 1.3 __END__
653 dpavlin 1.1 #-------------------------------------------------------------------------
654    
655 dpavlin 1.3
656     =head1 NAME
657    
658     psinib - Perl Snapshot Is Not Incremental Backup
659    
660     =head1 SYNOPSIS
661    
662 dpavlin 1.17 ./psinib.pl [OPTION]... [mount script]
663 dpavlin 1.3
664     =head1 DESCRIPTION
665    
666 dpavlin 1.17 Option can be one of more of following:
667    
668     =over 8
669    
670     =item C<--backupdest=dir>
671    
672     Specify backup destination directory (defaults is /data/
673    
674     =item C<--noping>
675    
676     Don't use ping to check if host is up (default is ti use tcp syn to cifs
677     port)
678    
679     =item C<--verbose -v>
680    
681     Increase verbosity level. Defailt is 1 which prints moderate amount of data
682     on STDOUT and STDERR.
683    
684     =item C<--quiet -q>
685    
686     Decrease verbosity level
687    
688 dpavlin 1.20 =item C<--email=email@domain>
689    
690     Send e-mails instead of dumping errors to STDERR. Useful for cron jobs.
691    
692 dpavlin 1.17 =back
693    
694 dpavlin 1.3 This script in current version support just backup of Samba (or Micro$oft
695     Winblowz) shares to central disk space. Central disk space is organized in
696     multiple directories named after:
697    
698     =over 4
699    
700     =item *
701     server which is sharing files to be backed up
702    
703     =item *
704     name of share on server
705    
706     =item *
707     dated directory named like standard ISO date format (YYYYMMDD).
708    
709     =back
710    
711     In each dated directory you will find I<snapshot> of all files on
712     exported share on that particular date.
713    
714     You can also use symlink I<latest> which will lead you to
715     last completed backup. After that you can use some other backup
716     software to transfer I<snapshot> to tape, CD-ROM or some other media.
717    
718     =head2 Design considerations
719    
720     Since taking of share snapshot every day requires a lot of disk space and
721     network bandwidth, B<psinib> uses several techniques to keep disk usage and
722     network traffic at acceptable level:
723    
724     =over 3
725    
726     =item - usage of hard-links to provide same files in each snapshot (as opposed
727     to have multiple copies of same file)
728    
729     =item - usage of file size, atime and mtime to find changes of files without
730     transferring whole file over network (just share browsing is transfered
731     over network)
732    
733     =item - usage of C<.md5sum> files (compatible with command-line utility
734 dpavlin 1.6 C<md5sum>) to keep file between snapshots hard-linked
735 dpavlin 1.3
736     =back
737    
738     =head1 CONFIGURATION
739    
740     This section is not yet written.
741    
742 dpavlin 1.4 =head1 HACKS, TRICKS, BUGS and LIMITATIONS
743    
744     This chapter will have all content that doesn't fit anywhere else.
745    
746     =head2 Can snapshots be more frequent than daily?
747 dpavlin 1.3
748     There is not real reason why you can't take snapshot more often than
749 dpavlin 1.4 once a day. Actually, if you are using B<psinib> to backup Windows
750     workstations you already know that they tend to come-and-go during the day
751     (reboots probably ;-), so running B<psinib> several times a day increases
752     your chance of having up-to-date backup (B<psinib> will not make multiple
753     snapshots for same day, nor will it update snapshot for current day if
754     it already exists).
755 dpavlin 1.3
756 dpavlin 1.4 However, changing B<psinib> to produce snapshots which are, for example, hourly
757 dpavlin 1.3 is a simple change of C<$DIR_TIME_FMT> which is currently set to
758     C<'%Y%m%d'> (see I<strftime> documentation for explanation of that
759     format). If you change that to C<'%Y%m%d-%H> you can have hourly snapshots
760     (if your network is fast enough, that is...). Also, some of messages in
761     program will sound strange, but other than that it should work.
762     I<You have been warned>.
763 dpavlin 1.4
764     =head2 Do I really need to share every directory which I want to snapshot?
765    
766     Actually, no. Due to usage of C<Filesys::SmbClient> module, you can also
767     specify sub-directory inside your share that you want to backup. This feature
768     is most useful if you want to use administrative shares (but, have in mind
769     that you have to enter your Win administrator password in unencrypted file on
770     disk to do that) like this:
771    
772     smbmount //server/c$/WinNT/fonts /mnt -o username=administrator%win
773    
774     After that you will get directories with snapshots like:
775    
776     server/c_WinNT_fonts/yyyymmdd/....
777    
778 dpavlin 1.6 =head2 Won't I run out of disk space?
779    
780     Of course you will... Snapshots and logfiles will eventually fill-up your disk.
781     However, you can do two things to stop that:
782    
783     =head3 Clean snapshort older than x days
784    
785     You can add following command to your C<root> crontab:
786    
787     find /backup/isis_backup -type d -mindepth 3 -maxdepth 3 -mtime +11 -exec rm -Rf {} \;
788    
789     I assume that C</backup/isis_backup> is directory in which are your snapshots
790     and that you don't want to keep snapshots older than 11 days (that's
791     C<-mtime +11> part of command).
792    
793     =head3 Rotate your logs
794    
795     I will leave that to you. I relay on GNU/Debian's C<logrotate> to do it for me.
796 dpavlin 1.7
797     =head2 What are I<YYYYMMDD.partial> directories?
798    
799     If there isn't I<latest> symlink in snapshot directory, it's preatty safe to
800     assume that previous backup from that day failed. So, that directory will
801     be renamed to I<YYYYMMDD.partial> and snapshot will be performed again,
802     linking same files (other alternative would be to erase that dir and find
803     second-oldest directory, but this seemed like more correct approach).
804 dpavlin 1.3
805 dpavlin 1.15 =head2 I can't connect to any share
806    
807     Please verify that nmblookup (which is part of samba package) is in /bin or
808     /usr/bin. Also verify that nmblookup returns IP address for your server
809     using:
810    
811     $ nmblookup tvhouse
812     querying tvhouse on 192.168.34.255
813     192.168.34.30 tvhouse<00>
814    
815     If you don't get any output, your samba might not listen to correct interface
816     (see interfaces in smb.conf).
817    
818 dpavlin 1.20 =head2 Aren't backups boring?
819    
820     No! If you have subjects.txt in same directory as C<psinib.pl> you can get
821     various funny subjects in your mail. They change over time as long as you
822     ignore your backup.
823    
824 dpavlin 1.3 =head1 AUTHOR
825    
826     Dobrica Pavlinusic <dpavlin@rot13.org>
827    
828 dpavlin 1.17 L<http:E<sol>E<sol>www.rot13.orgE<sol>~dpavlinE<sol>>
829 dpavlin 1.3
830     =head1 LICENSE
831    
832     This product is licensed under GNU Public License (GPL) v2 or later.
833    
834     =cut

  ViewVC Help
Powered by ViewVC 1.1.26