Subversion Repositories general

Rev

Rev 1154 | Rev 1206 | Go to most recent revision | Details | Compare with Previous | Last modification | View Log | RSS feed

Rev Author Line No. Line
1149 dev 1
#!/usr/bin/perl
2
# fsbackup - file system backup and synchronization utility. 
3
#
4
# http://www.opennet.ru/dev/fsbackup/
5
# Copyright (c) 2001-2002 by Maxim Chirkov. <mc@tyumen.ru>
6
#
1153 dev 7
# Ключи:
8
# -n - создаем новый архив независимо от состояния хэша.
9
# -f - full_backup - полный бэкап в архив, без хэша.
10
# -h - hash - только генерация хэша, без помещения файлов в архив.
11
# -c - clean - очиска хранилища с инкрементальным бэкапом и создание нового бэкапа.
1149 dev 12
 
13
#############################################
1154 dev 14
use constant DB_DEF_CACHE_SIZE => 40960000; # Размер кэша для размежения хэша в памяти
1149 dev 15
 
16
use POSIX;
17
use File::Find;
18
use Digest::MD5 qw(md5_base64);
19
use Net::FTP;
20
use DB_File;
1205 dev 21
use BSD::stat;
1149 dev 22
 
23
use constant VERB_SILENT => 0; # Silent mode, suspend all output.
24
use constant VERB_ERROR => 1; # Output all errors and warnings.
25
use constant VERB_ALL => 2; # Output all the  available  data.
26
 
1154 dev 27
my $version = "2.0";
1149 dev 28
my $list_lines_cnt = 0;
29
my $del_lines_cnt = 0;
30
my $cur_time = time();
31
my %active_hash_last;
32
my %active_hash_new;
33
my $cfg_new_flag = 0;
34
my $cfg_clean_flag = 0;
35
my $config = 0;
1153 dev 36
my $cur_backup_size = 1536; # Размер блока tar
1149 dev 37
my $backup_file_base;
38
my $prog_pgp_filter;
39
my $prog_gzip_filter;
40
my $arc_ext;
41
my $ftp;
42
my $cur_increment_level;
43
my $cur_dir;
44
my $cur_path;
45
my $cur_file;
46
my $cur_pathitem;
47
my $file_fullpath;
48
my $file_fullpath_md5;
49
my $key;
50
my $dbobj_new;
51
my $dbobj_last;
52
my $db_hashinfo;
53
my $db_hashinfo2;
54
my $file;
55
my @volume_position=(0);
1153 dev 56
my @fs_path=();	       #  /dir[/file] - путь к файлу/директории для бэкапа.
57
my @fs_notpath=();     #  ! - отрицание пути, не помещать в бэкап. Всегда должен быть первым символом.
58
my @fs_mask=();        #  =~ - маска для файла или директории, а не абсолютный путь. Первый или второй символ.
59
my @fs_filemask=();    #  f~ - маска для файла. Первый или второй символ.
60
my @fs_dirmask=();     #  d~ - маска для директории. Первый или второй символ.
61
my @fs_notmask=();     #  =! - "НЕ" маска для файла или директории, а не абсолютный путь. Первый или второй символ.
62
my @fs_notfilemask=(); #  f! - "НЕ" маска для файла. Первый или второй символ.
63
my @fs_notdirmask=();  #  d! - "НЕ" маска для директории. Первый или второй символ.
1149 dev 64
 
1153 dev 65
# ------------- Обработка параметров командной строки
1149 dev 66
 
67
if ($ARGV[0] eq "-n" || $ARGV[0] eq "-h" || $ARGV[0] eq "-f" || $ARGV[0] eq "-c"){
68
    $cfg_new_flag=1;
69
    $config = $ARGV[1];
70
} else {
71
    $cfg_new_flag=0;
72
    $config = $ARGV[0];
73
}
74
 
75
if ( ! -f $config){
76
    die "Usage: fsbackup.pl [-n|-f|-h|-c] config_name\n";
77
}
78
 
79
require "$config";
80
 
1154 dev 81
$cfg_move_old_backup=1 if(!defined($cfg_move_old_backup));
82
$cfg_exit_on_empty=1   if(!defined($cfg_exit_on_empty));
83
 
1149 dev 84
if ( ! -d $cfg_cache_dir){
85
    die "\$cfg_cache_dir ($cfg_cache_dir) not found. Set \$cfg_cache_dir varisble in fsbackup.pl\n";
86
}
87
 
1153 dev 88
$cfg_time_limit *= 60 * 60 * 24; # Дни в секунды.
89
$cfg_size_limit *= 1024;	 # Килобайты в байты.
90
$cfg_maximum_archive_size *= 1024;	 # Килобайты в байты.
1149 dev 91
 
1151 dev 92
chdir($cfg_root_path);
1149 dev 93
 
94
if ($ARGV[0] eq "-h"){
95
    $cfg_backup_style = "hash";
96
}
97
if ($ARGV[0] eq "-f" ){
98
    $cfg_backup_style = "full_backup";
99
}
100
 
101
if ($ARGV[0] eq "-c" ){
102
    $cfg_clean_flag=1;
103
} else {
104
    $cfg_clean_flag=0;
105
}
106
 
1153 dev 107
#------------------- Проверяем переменные в файле конфигурации.
1149 dev 108
if ($cfg_backup_name !~ /^[\w\d\_]+$/){
109
    die "Found illegal characters in $cfg_backup_name ($cfg_backup_name).";
110
}
111
 
112
if (! grep {$_ eq $cfg_checksum} ("md5", "timesize")){
113
    die "Unknown checksum method:\$cfg_checksum=$cfg_checksum (allowed md5 or timesize)\n";
114
}
115
 
116
if (! grep {$_ eq $cfg_backup_style} ("backup", "full_backup", "sync", "hash")){
117
    die "Unknown backup_style:\$cfg_backup_style=$cfg_backup_style\n";
118
}
119
 
120
 
121
if ($cfg_backup_style eq "full_backup" || $cfg_backup_style eq "hash"){
122
    $cfg_new_flag=1;
123
    $cfg_clean_flag=1;
124
}
125
 
126
if (! grep {$_ eq $cfg_type} ("local", "remote_ssh", "remote_ftp")){
127
    die "Unknown backup target:\$cfg_type=$cfg_type\n";
128
}
129
 
130
if (($cfg_type eq "local") && (! -d $cfg_local_path)){
131
    die "Can't find \$cfg_local_path ($cfg_local_path)";
132
}
133
 
134
if ($cfg_backup_style eq "backup"){
135
    my ($sec,$min,$hour,$mday,$mon,$year) = localtime($cur_time);
136
    $backup_file_base = sprintf ("%s-%4.4d.%2.2d.%2.2d.%2.2d.%2.2d.%2.2d",
137
		$cfg_backup_name,$year+1900,$mon+1,$mday,$hour,$min,$sec);
138
}else{
139
    $backup_file_base="$cfg_backup_name";
140
}
141
 
142
print "Creating $cfg_type $cfg_backup_style: $cfg_backup_name\n" if ($cfg_verbose == &VERB_ALL);
143
 
144
if ($cfg_pgp_userid ne "" && $prog_pgp ne ""){
145
    print "PGP: enabled\n" if ($cfg_verbose == &VERB_ALL);
146
 
147
#    PGP 2.6 (pgp)
148
#    $prog_pgp_filter="| $prog_pgp -ef $cfg_pgp_userid -z'$cfg_pgp_userid' ";
149
#    PGP 5.0 (pgpe)
150
#    $prog_pgp_filter="| $prog_pgp -f $cfg_pgp_userid";
151
#    GnuPG (pgp)
152
    $prog_pgp_filter="| $prog_pgp -v --batch -e -r $cfg_pgp_userid";
153
} else {
154
    $prog_pgp_filter="";
155
}
156
 
157
if ($prog_gzip ne ""){
158
    $prog_gzip_filter="| $prog_gzip";
159
    $arc_ext=".gz";
160
} else {
161
    $prog_gzip_filter="";
162
    $arc_ext="";
163
 
164
}
165
 
166
if (! -d "$cfg_cache_dir/$cfg_backup_name"){
167
    mkdir("$cfg_cache_dir/$cfg_backup_name", 0700);
168
}
169
 
1153 dev 170
# ---------- Активируем FTP соединение 
1149 dev 171
 
172
ftp_connect();
173
 
1153 dev 174
#----------- Вычисляем уровень инкрементальности.
1149 dev 175
if ($cfg_increment_level != 0 && $cfg_backup_style eq "backup"){
1154 dev 176
    if(open(INCREMENT_LEVEL, "<$cfg_cache_dir/$cfg_backup_name/.increment_level")) {
177
        $cur_increment_level = <INCREMENT_LEVEL>;
178
        $cur_increment_level++;
179
        close (INCREMENT_LEVEL);
180
    }
181
    else {
182
        print "Can't open increment level file ($cfg_cache_dir/$cfg_backup_name/.increment_level).\n";
183
        $cur_increment_level = 0;
184
    }
1152 dev 185
 
1154 dev 186
    if ($cur_increment_level >= $cfg_increment_level){
187
        $cur_increment_level=0;
188
    }
1152 dev 189
 
1154 dev 190
    if ($cur_increment_level == 0){
1149 dev 191
	$cfg_new_flag=1;
192
	$cfg_clean_flag=1;
193
    }
1154 dev 194
    if(open(INCREMENT_LEVEL, ">$cfg_cache_dir/$cfg_backup_name/.increment_level")) {
195
        print INCREMENT_LEVEL $cur_increment_level;
196
        close (INCREMENT_LEVEL);
197
    }
198
    else {
199
        print "Can't save increment level to file ($cfg_cache_dir/$cfg_backup_name/.increment_level).\n";
200
    }
1149 dev 201
    print "Current increment number: $cur_increment_level\n" if ($cfg_verbose == &VERB_ALL);
202
}
203
################################################
1153 dev 204
#----------- Считываем хэш в память.
1149 dev 205
 
206
if ( (-f "$cfg_cache_dir/$cfg_backup_name/.hash" || $cfg_type ne "local" ) && $cfg_new_flag == 0){
1153 dev 207
# Считываем текущий хеш в память.
1149 dev 208
 
209
if ( $cfg_type eq "local"){
210
    rename ("$cfg_cache_dir/$cfg_backup_name/.hash", "$cfg_cache_dir/$cfg_backup_name/.hash.last");
211
}elsif ( $cfg_type eq "remote_ssh"){
212
    system ("$prog_ssh -l $cfg_remote_login $cfg_remote_host 'cat $cfg_remote_path/.hash' > $cfg_cache_dir/$cfg_backup_name/.hash.last") == 0 || print "SSH connection failed: $?\n";
213
} elsif ( $cfg_type eq "remote_ftp"){
214
    unlink ("$cfg_cache_dir/$cfg_backup_name/.hash.last");
215
    $ftp->get(".hash", "$cfg_cache_dir/$cfg_backup_name/.hash.last")|| print "FTP error, Can't GET .hash\n";
216
}
217
        $db_hashinfo = new DB_File::HASHINFO ;
218
        $db_hashinfo->{'cachesize'} =  DB_DEF_CACHE_SIZE;
219
        if (! ($dbobj_last = tie(%active_hash_last, "DB_File", "$cfg_cache_dir/$cfg_backup_name/.hash.last", O_RDWR|O_CREAT, 0644, $db_hashinfo ))){
220
	    print "WARNING: Error in hash, creating full backup.\n" if ($cfg_verbose >= &VERB_ERROR);
221
	    unlink "$cfg_cache_dir/$cfg_backup_name/.hash.last";
222
	    $dbobj_last = tie(%active_hash_last, "DB_File", "$cfg_cache_dir/$cfg_backup_name/.hash.last", O_RDWR|O_CREAT, 0644, $db_hashinfo )||print "Can't create or open DB File!";
223
	}
224
	# $dbobj->del($key);
225
	# $dbobj->sync();
226
 
227
}
228
 
1153 dev 229
# Закрываем ftp соединение. Следующий блок может выполняться гораздо дольше 
230
# чем таймаут ftp.
1149 dev 231
if ( $cfg_type eq "remote_ftp"){
232
    $ftp->quit;
233
}
1153 dev 234
#Создаем новый хеш.
1149 dev 235
unlink("$cfg_cache_dir/$cfg_backup_name/.hash");
236
$db_hashinfo2 = new DB_File::HASHINFO ;
237
$db_hashinfo2->{'cachesize'} =  100000;
238
$dbobj_new = tie(%active_hash_new, "DB_File", "$cfg_cache_dir/$cfg_backup_name/.hash", O_RDWR|O_CREAT, 0644, $db_hashinfo2) || print "Can't create or open DB File!\n";
239
 
1153 dev 240
# Создаем список файлов для помещения в архив.
1149 dev 241
open (LIST, ">$cfg_cache_dir/$cfg_backup_name/$cfg_backup_name.list")|| print "Can't create list file ($cfg_cache_dir/$cfg_backup_name/$cfg_backup_name.list).\n";
242
flock (LIST, 2);
243
 
1153 dev 244
# Создаем список директорий в архиве.
1149 dev 245
open (DIRS, ">$cfg_cache_dir/$cfg_backup_name/$cfg_backup_name.dir")|| print "Can't create list file ($cfg_cache_dir/$cfg_backup_name/$cfg_backup_name.dir).\n";
246
flock (DIRS, 2);
247
 
1154 dev 248
open (META, ">$cfg_cache_dir/$cfg_backup_name/$cfg_backup_name.meta")|| print "Can't create meta file ($cfg_cache_dir/$cfg_backup_name/$cfg_backup_name.meta).\n";
249
print META "fsbackup $version\n";
250
print META "increment level: $cur_increment_level\n";
251
close(META);
252
 
1153 dev 253
# Считываем список подлежащих бэкапу директорий в память.
1151 dev 254
 
1149 dev 255
while(<DATA>){
256
    chomp;
257
    $cur_path = $_;
258
    if ($cur_path =~ /^\!(.*)$/){		#  !
259
	push @fs_notpath, $1;
260
 
261
    } elsif ($cur_path =~ /^\=\~(.*)$/){	#  =~
262
	push @fs_mask, $1;
263
 
264
    } elsif ($cur_path =~ /^f\~(.*)$/){		#  f~
265
	push @fs_filemask, $1;
266
 
267
    } elsif ($cur_path =~ /^d\~(.*)$/){		#  d~
268
	push @fs_dirmask, $1;
269
 
270
    } elsif ($cur_path =~ /^\=\!(.*)$/){	#  =!
271
	push @fs_notmask, $1;
272
 
273
    } elsif ($cur_path =~ /^f\!(.*)$/){		#  f!
274
	push @fs_notfilemask, $1;
275
 
276
    } elsif ($cur_path =~ /^d\!(.*)$/){		#  d!
277
	push @fs_notdirmask, $1;
278
 
279
    } elsif ($cur_path =~ /^#/ || $cur_path =~ /^\s*$/){ #  comment
280
	next;
281
 
282
    } elsif ($cur_path =~ /[\/\w]+/) {		#  /dir[/file]
1151 dev 283
	push @fs_path, $cur_path;
1149 dev 284
 
285
    } else {
286
	print STDERR "Syntax error: $cur_path, ingnored.\n" if ($cfg_verbose >= &VERB_ALL);
287
    }
288
}
289
 
290
#--------------------------------------------------------------------
1153 dev 291
# Последовательно просматририваем весь список директорий отмеченных для бэкапа
1149 dev 292
 
293
 
294
foreach $cur_pathitem (@fs_path){
295
    print "Adding $cur_pathitem....\n" if ($cfg_verbose == &VERB_ALL);
296
    find (\&add_to_backup, $cur_pathitem);
297
    print "done\n" if ($cfg_verbose == &VERB_ALL);
298
}
299
close (LIST);
300
close (DIRS);
301
#------------
1153 dev 302
# Составляем список удаленных файлов.
1149 dev 303
 
304
    open (DEL, ">$cfg_cache_dir/$cfg_backup_name/$cfg_backup_name.del")|| print "Can't create list file ($cfg_cache_dir/$cfg_backup_name/$cfg_backup_name.del).\n";
305
    flock (DEL, 2);
306
    if ($cfg_backup_style ne "hash"){
307
	while(($file, $key)= each(%active_hash_last)){
308
	    $file =~ s/\'/\'\\\'\'/g;
309
	    $file =~ s/^\/(.*)$/$1/;
310
	    print DEL "rm -rf '$file'\n";
311
	    $del_lines_cnt++;
312
	}
313
    }
314
    close(DEL);
315
 
1153 dev 316
# Записываем хэш на диск.
1149 dev 317
$dbobj_new->sync();
318
untie %active_hash_new;
319
untie %active_hash_last;
320
 
1153 dev 321
chdir ("/"); # Переходим в корень, так как все пути у нас без корневого /
1149 dev 322
 
1153 dev 323
# Активируем FTP соединение второй раз.
1149 dev 324
ftp_connect();
325
 
326
#------------
1153 dev 327
# Если только обновляем хэш, то записываем его и выходим.
1149 dev 328
 
1153 dev 329
if ($cfg_backup_style eq "hash"){ # Только создать хэшь без архивирования.
1149 dev 330
 
331
    if ( $cfg_type eq "local"){
332
	system( "cp -f $cfg_cache_dir/$cfg_backup_name/.hash $cfg_local_path/.hash") == 0 || print "Local FS copy hash failed: $?";
333
    } elsif ( $cfg_type eq "remote_ssh"){
334
	system( "cat $cfg_cache_dir/$cfg_backup_name/.hash | $prog_ssh -l $cfg_remote_login $cfg_remote_host 'cat - > $cfg_remote_path/.hash'") == 0 || print "SSH connection failed (copy hash): $?\n";
335
    } elsif ( $cfg_type eq "remote_ftp"){
336
	$ftp->delete(".hash");
337
	$ftp->put("$cfg_cache_dir/$cfg_backup_name/.hash", ".hash")|| print "Can't upload .hash to remote server via FTP\n";
338
    }
339
    exit (0);
340
}
341
 
342
#------------
1153 dev 343
# Архивируем и передаем в хранилище.
1149 dev 344
 
1154 dev 345
if ($cfg_exit_on_empty == 1 && $list_lines_cnt == 0 && $del_lines_cnt == 0){
346
print "$cfg_exit_on_empty\n";
1149 dev 347
    print "WARNING: Nothing to backup.\n" if ($cfg_verbose >= &VERB_ALL);
348
    exit;
349
}
350
if ( $cfg_type eq "local"){
351
 
352
    print "Storing local backup...\n" if ($cfg_verbose == &VERB_ALL);
353
    if ($cfg_backup_style eq "sync"){
1153 dev 354
	if ($cfg_clean_flag == 1){ # Удалить старые копии
1151 dev 355
	    print "WARNING: If you really shure to delete $cfg_local_path before sync operatioun uncomment line 'system( \"find \$cfg_local_path -not -path '\$cfg_local_path' -maxdepth 1 -exec \$prog_rm -rf \{\} \\;\");'" if ($cfg_verbose >= &VERB_ALL);
356
#    	    system( "find $cfg_local_path -not -path '$cfg_local_path' -maxdepth 1 -exec $prog_rm -rf \{\} \\;");
1149 dev 357
	}
358
 
359
	system( "cd $cfg_local_path; sh $cfg_cache_dir/$cfg_backup_name/$cfg_backup_name.del");
360
	system( "$prog_tar -c -f - -T $cfg_cache_dir/$cfg_backup_name/$cfg_backup_name.list| $prog_tar -xf - -C $cfg_local_path") == 0 || print "Local FS sync failed (tar|untar): $?\n";
361
	system( "cd $cfg_local_path; sh $cfg_cache_dir/$cfg_backup_name/$cfg_backup_name.dir");
362
	system( "cp -f $cfg_cache_dir/$cfg_backup_name/.hash $cfg_local_path/$backup_file_base.hash") == 0 || print "Local FS copy failed: $?\n";
363
 
364
    } else {
1153 dev 365
	if ($cfg_clean_flag == 1){ # Удалить старые копии
1149 dev 366
	    if ($cfg_save_old_backup == 0){
367
		system( "$prog_rm -f $cfg_local_path/*");
1154 dev 368
	    } elsif($cfg_move_old_backup == 1) {
1149 dev 369
		if (! -d "$cfg_local_path/OLD"){
370
		    system( "mkdir $cfg_local_path/OLD");
371
		}
372
		system( "$prog_rm -f $cfg_local_path/OLD/*");
373
		system( "mv -f $cfg_local_path/$cfg_backup_name* $cfg_local_path/OLD/");
374
		# system( "$prog_rm -f $cfg_local_path/*");
375
	    }
376
	}
1154 dev 377
	system( "cp -f $cfg_cache_dir/$cfg_backup_name/$cfg_backup_name.meta $cfg_local_path/$backup_file_base.meta") == 0 || print "Local FS .meta copy failed: $?\n";
1149 dev 378
	system( "cp -f $cfg_cache_dir/$cfg_backup_name/$cfg_backup_name.list $cfg_local_path/$backup_file_base.list") == 0 || print "Local FS .list copy failed: $?\n";
379
	system( "cp -f $cfg_cache_dir/$cfg_backup_name/$cfg_backup_name.dir $cfg_local_path/$backup_file_base.dir") == 0 || print "Local FS .dir copy failed: $?\n";
380
	system( "cp -f $cfg_cache_dir/$cfg_backup_name/$cfg_backup_name.del $cfg_local_path/$backup_file_base.del") == 0 || print "Local FS .del copy failed: $?\n";
1154 dev 381
	#system( "cp -f $cfg_cache_dir/$cfg_backup_name/.hash $cfg_local_path/$backup_file_base.hash") == 0 || print "Local FS .hash copy failed: $?\n";
1153 dev 382
	# Обрабатываем разбиение на тома
1149 dev 383
	for ($arc_block_level=0; $arc_block_level <= $#volume_position; $arc_block_level++){
1154 dev 384
	    my $tmp_list_file = create_tmp_list($arc_block_level, $volume_position[$arc_block_level], $volume_position[$arc_block_level+1], "$cfg_cache_dir/$cfg_backup_name/$cfg_backup_name.list");
1149 dev 385
	    system( "$prog_tar -c -f - -T $tmp_list_file $prog_gzip_filter $prog_pgp_filter > $cfg_local_path/$backup_file_base-$arc_block_level.tar${arc_ext}") == 0 || print "Local FS tar backup failed: $?\n";
386
	}
387
    }
388
 
389
} elsif ( $cfg_type eq "remote_ssh"){
390
    print "Storing remote ssh backup...\n" if ($cfg_verbose == &VERB_ALL);
391
    if ($cfg_backup_style eq "sync"){
1153 dev 392
	if ($cfg_clean_flag == 1){ # Удалить старые копии
1151 dev 393
	    system( "$prog_ssh -l $cfg_remote_login $cfg_remote_host find $cfg_remote_path -not -path '$cfg_remote_path' -maxdepth 1 -exec rm -rf \{\} \\;");
1149 dev 394
	}
395
	system( "cat $cfg_cache_dir/$cfg_backup_name/.hash | $prog_ssh -l $cfg_remote_login $cfg_remote_host 'cat - > $cfg_remote_path/.hash'") == 0 || print "SSH connection failed (store .hash): $?\n";
396
	system( "cat $cfg_cache_dir/$cfg_backup_name/$cfg_backup_name.del | $prog_ssh -l $cfg_remote_login $cfg_remote_host 'cat - > $cfg_remote_path/.del'") == 0 || print "SSH connection failed (store .hash): $?\n";
397
	system( "cat $cfg_cache_dir/$cfg_backup_name/$cfg_backup_name.dir | $prog_ssh -l $cfg_remote_login $cfg_remote_host 'cat - > $cfg_remote_path/.dir'") == 0 || print "SSH connection failed (store .hash): $?\n";
398
        system("$prog_ssh -l $cfg_remote_login $cfg_remote_host '(cd $cfg_remote_path; sh .del)'");
399
        system( "$prog_tar -c -f - -T $cfg_cache_dir/$cfg_backup_name/$cfg_backup_name.list $prog_gzip_filter| $prog_ssh -l $cfg_remote_login $cfg_remote_host tar -xf - -C $cfg_remote_path") == 0 || print "SSH connection failed (tar): $?\n";;
400
        system("$prog_ssh -l $cfg_remote_login $cfg_remote_host '(cd $cfg_remote_path; sh .dir)'");
401
 
402
 
403
	open (DEL, "<$cfg_cache_dir/$cfg_backup_name/$cfg_backup_name.del");
404
	flock (DEL, 1);
405
	while(<DEL>){
406
	    chomp;
407
	    $cur_file = $_;
408
	    $cur_file =~ s/\'/\'\\\'\'/g;
409
    	    system("$prog_ssh -l $cfg_remote_login $cfg_remote_host rm -f '$cfg_remote_path/$cur_file'");
410
	}
411
	close(DEL);
412
    } else {
1153 dev 413
	if ($cfg_clean_flag == 1){ # Удалить старые копии
1149 dev 414
 
415
	    if ($cfg_save_old_backup == 0){
416
		system( "$prog_ssh -l $cfg_remote_login $cfg_remote_host rm -f $cfg_remote_path/*");
417
	    } else {
418
		system( "$prog_ssh -l $cfg_remote_login $cfg_remote_host '(if [ ! -d $cfg_remote_path/OLD ]; then mkdir $cfg_remote_path/OLD; fi)'");
419
		system( "$prog_ssh -l $cfg_remote_login $cfg_remote_host rm -f $cfg_remote_path/OLD/*");
420
		system( "$prog_ssh -l $cfg_remote_login $cfg_remote_host mv -f $cfg_remote_path/$cfg_backup_name* $cfg_remote_path/OLD/");
421
    		# system( "$prog_ssh -l $cfg_remote_login $cfg_remote_host rm -f $cfg_remote_path/*");
422
	    }
423
	}
424
	system( "cat $cfg_cache_dir/$cfg_backup_name/$cfg_backup_name.list | $prog_ssh -l $cfg_remote_login $cfg_remote_host 'cat - > $cfg_remote_path/$backup_file_base.list'") == 0 || print "SSH connection failed (copy .list): $?\n";
425
	system( "cat $cfg_cache_dir/$cfg_backup_name/$cfg_backup_name.dir | $prog_ssh -l $cfg_remote_login $cfg_remote_host 'cat - > $cfg_remote_path/$backup_file_base.dir'") == 0 || print "SSH connection failed (copy .dir): $?\n";
426
        system( "cat $cfg_cache_dir/$cfg_backup_name/$cfg_backup_name.del | $prog_ssh -l $cfg_remote_login $cfg_remote_host 'cat - > $cfg_remote_path/$backup_file_base.del'") == 0 || print "SSH connection failed (copy .del): $?\n";
427
        system( "cat $cfg_cache_dir/$cfg_backup_name/.hash | $prog_ssh -l $cfg_remote_login $cfg_remote_host 'cat - > $cfg_remote_path/$backup_file_base.hash'") == 0 || print "SSH connection failed (copy .hash): $?\n";
428
	system( "cat $cfg_cache_dir/$cfg_backup_name/.hash | $prog_ssh -l $cfg_remote_login $cfg_remote_host 'cat - > $cfg_remote_path/.hash'") == 0 || print "SSH connection failed (cache .hash): $?\n";
1153 dev 429
	# Обрабатываем разбиение на тома
1149 dev 430
	for ($arc_block_level=0; $arc_block_level <= $#volume_position; $arc_block_level++){
1154 dev 431
	    my $tmp_list_file = create_tmp_list($arc_block_level, $volume_position[$arc_block_level], $volume_position[$arc_block_level+1], "$cfg_cache_dir/$cfg_backup_name/$cfg_backup_name.list");
1149 dev 432
            system( "$prog_tar -c -f - -T $tmp_list_file $prog_gzip_filter $prog_pgp_filter| $prog_ssh -l $cfg_remote_login $cfg_remote_host 'cat - > $cfg_remote_path/$backup_file_base-$arc_block_level.tar${arc_ext}'") == 0 || print "SSH connection failed (tar): $?\n";
433
	}
434
    }
435
} elsif ( $cfg_type eq "remote_ftp"){
436
    print "Storing remote ftp backup...\n" if ($cfg_verbose == &VERB_ALL);
437
 
438
    if ($cfg_backup_style eq "sync"){
439
	print "WARNING: Backup style 'sync' only allowed for local and remote_ssh storage.\n" if ($cfg_verbose >= &VERB_ALL);
440
    } else {
1153 dev 441
	if ($cfg_clean_flag == 1){ # Удалить старые копии
1149 dev 442
	    if ($cfg_save_old_backup == 0){
443
		foreach $cur_dir ($ftp->ls()){
444
    		    $ftp->delete($cur_dir);
445
		}
446
	    } else {
447
		$ftp->mkdir("$cfg_remote_path/OLD");
448
		$ftp->cwd("$cfg_remote_path/OLD");
449
		foreach $cur_dir ($ftp->ls()){
450
    		    $ftp->delete($cur_dir);
451
		}
452
		$ftp->cwd("$cfg_remote_path");
453
		foreach $cur_dir ($ftp->ls()){
454
		    if ($cur_dir =~ /$cfg_backup_name/){
455
    			$ftp->rename($cur_dir,"$cfg_remote_path/OLD/$cur_dir");
456
		    }
457
		}
458
		foreach $cur_dir ($ftp->ls()){
459
    		    $ftp->delete($cur_dir);
460
		}
461
	    }
462
	}
463
	$ftp->delete("$backup_file_base.list");
464
	$ftp->put("$cfg_cache_dir/$cfg_backup_name/$cfg_backup_name.list", "$backup_file_base.list") || print "Can't PUT .list file to remote FTP server\n";
465
	$ftp->delete("$backup_file_base.dir");
466
	$ftp->put("$cfg_cache_dir/$cfg_backup_name/$cfg_backup_name.dir", "$backup_file_base.dir")|| print "Can't PUT .dir file to remote FTP server\n";
467
	$ftp->delete("$backup_file_base.del");
468
	$ftp->put("$cfg_cache_dir/$cfg_backup_name/$cfg_backup_name.del", "$backup_file_base.del")|| print "Can't PUT .del file to remote FTP server\n";
469
        $ftp->delete("$backup_file_base.hash");
470
        $ftp->put("$cfg_cache_dir/$cfg_backup_name/.hash", "$backup_file_base.hash")|| print "Can't PUT old .hash file to remote FTP server\n";
471
	$ftp->delete(".hash");
472
	$ftp->put("$cfg_cache_dir/$cfg_backup_name/.hash", ".hash")|| print "Can't PUT new .hash file to remote FTP server\n";
1153 dev 473
	# Обрабатываем разбиение на тома
1149 dev 474
	for ($arc_block_level=0; $arc_block_level <= $#volume_position; $arc_block_level++){
1154 dev 475
	    my $tmp_list_file = create_tmp_list($arc_block_level, $volume_position[$arc_block_level], $volume_position[$arc_block_level+1], "$cfg_cache_dir/$cfg_backup_name/$cfg_backup_name.list");
1149 dev 476
	    $ftp->delete("$backup_file_base-$arc_block_level.tar${arc_ext}");
477
	    open (TAR,"$prog_tar -c -f - -T $tmp_list_file $prog_gzip_filter $prog_pgp_filter|")|| print "tar failed: $?\n";
478
    	    flock(TAR,1);
479
	    $ftp->put(*TAR, "$backup_file_base-$arc_block_level.tar${arc_ext}")|| print "Can't store backup archive to remote FTP server.\n";
480
	    close(TAR);
481
	}
482
    	$ftp->quit;
483
    }
484
}
485
 
486
if ( $cfg_type eq "remote_ftp"){
487
    $ftp->quit;
488
}
489
print "***** Backup successful complete.\n" if ($cfg_verbose == &VERB_ALL);
490
exit (0);
491
 
492
 
493
########################################
494
sub add_to_backup{
495
  my($file_name, $file_dir, $md5_checksum_stat, $checksum_stat);
1205 dev 496
  my($tmp, $stat_mode, $stat_uid, $stat_gid, $stat_size, $stat_mtime, $stat_time, $stat_flags);
1149 dev 497
 
498
  $file_name  = $_;
499
  $file_fullpath  = $File::Find::name;
500
  $file_dir  = $File::Find::dir;
501
  my $file_fullpath_esc = $file_fullpath;
502
  $file_fullpath_esc =~ s/\'/\'\\\'\'/g;
503
 
1153 dev 504
  # Создаем список директорий
1151 dev 505
  if ((-d $file_fullpath) && (! -l $file_fullpath)){
1149 dev 506
      if (check_path($file_dir, $file_name) == 1){
507
	if ($cfg_backup_style ne "hash"){
1205 dev 508
	   ($tmp, $tmp, $stat_mode, $tmp, $stat_uid, $stat_gid, $tmp, $stat_size, $tmp, $stat_mtime, $stat_time, $tmp, $tmp, $tmp, $tmp, $tmp, $stat_flags, $tmp) = stat($file_fullpath);
509
	    unless($stat_flags & UF_NODUMP) {
510
	        $stat_mode = sprintf ("%04o", $stat_mode & 07777);
511
	        $file_fullpath_esc =~ s/^\/(.*)$/$1/;
512
	        my ($sec,$min,$hour,$mday,$mon,$year) = localtime($stat_time);
513
	        $stat_time = sprintf ("%4.4d%2.2d%2.2d%2.2d%2.2d.%2.2d",
514
		                      $year+1900,$mon+1,$mday,$hour,$min,$sec);
515
    	        print DIRS "mkdir -p '$file_fullpath_esc'\n";
516
    	        print DIRS "chmod $stat_mode '$file_fullpath_esc'\n";
517
    	        print DIRS "chown $stat_uid:$stat_gid '$file_fullpath_esc'\n";
518
    	        print DIRS "touch -t $stat_time '$file_fullpath_esc'\n";
519
	        $cur_backup_size += int(length($file_fullpath)/100.0 + 1)*512;
520
	        if ($cfg_maximum_archive_size > 0 && $cur_backup_size + 10240 >= $cfg_maximum_archive_size){
521
	            my $old_val = $cur_backup_size - $stat_size - int(length($file_fullpath)/100.0 + 1)*512;
522
		    my $tmp_pos= $#volume_position+1;
523
	            print "Volume $tmp_pos Done. Size: $old_val\n" if ($cfg_verbose == &VERB_ALL);
524
		    $cur_backup_size = $stat_size + int(length($file_fullpath)/100.0 + 1)*512 + 1536;
525
	    	    push @volume_position, $list_lines_cnt;
526
	        }
527
	        $active_hash_new{$file_fullpath} = "D";
528
	        check_update($file_fullpath, "D", $file_fullpath, $stat_size);
1149 dev 529
	    }
1205 dev 530
	    else {
531
	        push @fs_notpath, "$file_dir/$file_name"; # if nodump - skip all subitems
532
                if ($cfg_stopdir_prune == 1){
533
                    $File::Find::prune = 1;
534
                    return;
535
	        }
536
	    }
1149 dev 537
	}
538
      } else {
539
          if ($cfg_stopdir_prune == 1){
540
              $File::Find::prune = 1;
541
              return;
542
	  }
543
      }
1205 dev 544
    # Работаем с файлами
545
    } elsif ((-f $file_fullpath) || (-l $file_fullpath)){
546
        if (check_path($file_dir, $file_name) == 1){
547
	    ($tmp, $tmp, $stat_mode, $tmp, $stat_uid, $stat_gid, $tmp, $stat_size, $tmp, $stat_mtime, $stat_time, $tmp, $tmp, $tmp, $tmp, $tmp, $stat_flags, $tmp) = stat($file_fullpath);
548
	    unless($stat_flags & UF_NODUMP) {
549
	        $checksum_stat= md5_base64("$stat_mtime/$stat_size/$stat_mode/$stat_uid/$stat_gid");
550
	        # $file_fullpath_md5 = md5_base64($file_fullpath);
551
	        $file_fullpath_md5 = $file_fullpath;
552
	        if ($cfg_time_limit != 0 && $cur_time - $cfg_time_limit > $stat_mtime){
553
	            print "Time limit: $cur_time - $cfg_time_limit > $stat_mtime, file $file_fullpath ignored.\n" if ($cfg_verbose == &VERB_ALL);
554
	            next;
555
	        }
556
	        if ($cfg_size_limit != 0 && $cfg_size_limit < $stat_size){
557
	            print "Size limit: $cfg_size_limit < $stat_size, file $file_fullpath ignored.\n" if ($cfg_verbose == &VERB_ALL);
558
	            next;
559
	        }
1149 dev 560
 
1205 dev 561
	        if (($cfg_checksum eq "md5") && (! -l $file_fullpath)){
562
	            ($md5_checksum_stat, $tmp) = split(/\s+/, `$prog_md5sum '$file_fullpath_esc'`);
563
	            $active_hash_new{$file_fullpath_md5} = "$checksum_stat/$md5_checksum_stat";
564
	            check_update($file_fullpath, "$checksum_stat/$md5_checksum_stat", $file_fullpath_md5, $stat_size);
565
	        } else {
566
	            $active_hash_new{$file_fullpath} = $checksum_stat;
567
	            check_update($file_fullpath, $checksum_stat, $file_fullpath, $stat_size);
568
	        }
569
            }
570
        }
571
    }
1149 dev 572
}
573
 
574
###############################################
1153 dev 575
# Проверяем изменился ли файл или нет, если да апдейтим лог.
1149 dev 576
sub check_update{
577
     my ($file, $checksum, $filesum, $stat_size) = @_;
578
 
1154 dev 579
    if (($active_hash_last{$filesum} ne $checksum) && ($checksum ne "D")){
1149 dev 580
	if ($cfg_backup_style ne "hash"){
581
		$file =~ s/^\/(.*)$/$1/;
582
	        print LIST "$file\n";
583
 
1153 dev 584
	        # Обрабатываем случай разбиения гиганских архивов.
1149 dev 585
		if (-l "/$file"){
586
		    $stat_size = 0;
587
		}
588
	        $cur_backup_size += $stat_size + int(length($file)/100.0 + 1)*512;
589
#	  	print "$cur_backup_size:$stat_size:$file\n";
590
	        if ($cfg_maximum_archive_size > 0 && $cur_backup_size + 10240 >= $cfg_maximum_archive_size){
591
	        my $old_val = $cur_backup_size - $stat_size - int(length($file)/100.0 + 1)*512;
592
		my $tmp_pos= $#volume_position+1;
593
	        print "Volume $tmp_pos Done. Size: $old_val\n" if ($cfg_verbose == &VERB_ALL);
594
	        $cur_backup_size = $stat_size + int(length($file)/100.0 + 1)*512 + 1536;
595
	        push @volume_position, $list_lines_cnt;
596
	  }
597
 
598
	}
599
	$list_lines_cnt++;
600
    }
1154 dev 601
    if(($active_hash_last{$filesum} eq "D") && ($checksum ne "D")
602
      || ($active_hash_last{$filesum} ne "D") && ($checksum eq "D"))
603
    {
604
        # if old entry was a directory and now it's file or link or vice versa, leave it in hash 
605
        # to add it later to the delete list
1149 dev 606
    }
1154 dev 607
    else {
608
        delete $active_hash_last{$filesum};
609
        if (defined $dbobj_last){
610
	    $dbobj_last->del($filesum);
611
        }
612
    }
1149 dev 613
}
614
 
615
###############################################
1153 dev 616
# 0 - не добавлять файл
617
# 1 - добавть файл
1149 dev 618
 
619
sub check_path {
620
    my ($dir_name, $file_name) = @_;
621
    my ($item, $path);
622
 
623
    $path = "$dir_name/$file_name";
624
 
1151 dev 625
 
626
 
1149 dev 627
    foreach $item (@fs_notmask){
628
	if ($path =~ /$item/){
629
	    return 0;
630
	}
631
    }
632
 
633
    foreach $item (@fs_notfilemask){
634
	if ($file_name =~ /$item/){
635
	    return 0;
636
	}
637
    }
638
 
639
    foreach $item (@fs_filemask){
640
	if ($file_name =~ /$item/){
641
	    return 1;
642
	}
643
    }
644
 
645
    foreach $item (@fs_notdirmask){
646
	if ($dir_name =~ /$item/){
647
	    return 0;
648
	}
649
    }
650
 
651
 
652
    foreach $item (@fs_mask){
653
	if ($path =~ /$item/){
654
	    return 1;
655
	}
656
    }
657
 
658
    foreach $item (@fs_dirmask){
659
	if ($dir_name =~ /$item/){
660
	    return 1;
661
	}
662
    }
663
 
664
 
665
    foreach $item (@fs_notpath){
666
	if (($dir_name eq $item) || ($path eq $item) || ($dir_name =~ /^$item\//)){
667
	    return 0;
668
	}
669
    }
670
 
671
    return 1;
672
}
673
###############################################
1153 dev 674
# Устанавливаем соединение с удаленным сервером по FTP.
1149 dev 675
 
676
sub ftp_connect{
677
    if ( $cfg_type eq "remote_ftp"){
1151 dev 678
	$ftp = Net::FTP->new($cfg_remote_host, Timeout => 30, Debug => 0) || die "Can't connect to ftp server.\n";
1149 dev 679
	$ftp->login($cfg_remote_login, $cfg_remote_password) || die "Can't login to ftp server.\n";
680
        $ftp->cwd($cfg_remote_path) || die "Path $cfg_remote_path not found on ftp server.\n";
681
	$ftp->binary();    
682
    }
683
}
684
###############################################
1153 dev 685
# Содание списка файлов для помещения в определенный том многотомного архива.
1149 dev 686
 
1154 dev 687
sub create_tmp_list{
1149 dev 688
	my ($arc_block_level, $position1, $position2, $full_list_path) = @_;
689
	my ($tmp_list_path, $pos_counter);
690
 
691
    if ($arc_block_level == 0 && $position1 == 0 && $position2 eq ''){
692
	$tmp_list_path = $full_list_path;
693
    } else {
694
	$pos_counter = 0;
695
	$tmp_list_path = "$full_list_path.$arc_block_level";
696
	open(FULL_LIST, "<$full_list_path")|| die "Can't open full list $full_list_path\n";
697
	flock(FULL_LIST, 1);
698
	open(TMP_LIST, ">$tmp_list_path")|| die "Can't create temp list $tmp_list_path\n";
699
	flock(TMP_LIST, 2);
700
	while(<FULL_LIST>){
701
	    if (($pos_counter >= $position1) && ($pos_counter < $position2 || $position2 eq '')){
702
		print TMP_LIST $_;
703
	    }
704
	    $pos_counter++;
705
	}
706
	close(TMP_LIST);
707
	close(FULL_LIST);
708
    }
709
    return $tmp_list_path;
710
}
711
###############################################
712
###############################################
713
 
714
__END__
715
 
716
=head1 NAME
717
 
718
fsbackup - file system backup and synchronization utility. 
719
 
720
=head1 SYNOPSIS
721
 
722
    fsbackup.pl [options] <configuration file>
723
 
724
=head1 DESCRIPTION
725
 
726
C<fsbackup.pl> is a incremental backup creation utility. 
727
C<fsbackup.pl> support backup compression and encryption. Backup can be stored
728
on local file system and on remote host stored over SSH or FTP. Some addition 
729
scripts allow backups SQL tables from PostgreSQL and MySQL (C<pgsql_backup.sh> 
730
and C<mysql_backup.sh>)), save system configuration files and list of installed 
731
packages (C<sysbackup.sh>). 
732
Backuped with C<fsbackup.pl> files can be recovered by script C<fsrestore.sh>,
733
backuped with C<sysbackup.sh> system packeges can be reinstalled by C<sysrestore.sh>
734
 
735
=head1 OPTIONS
736
 
737
The following command-line options can be used with C<fsbackup.pl>:
738
 
739
=over
740
 
741
=item C<-n>
742
 
743
Create new backup without checking files in previously stored hash.
744
 
745
=item C<-f>
746
 
747
Create full backup, like as C<-n> option.
748
 
749
=item C<-h>
750
 
751
Only rebuild hash, no storing files in backup archive.
752
 
753
=item C<-c>
754
 
755
Clean incremental backup storage and create new full backup without checking
756
$cfg_increment_level config parameter.
757
 
758
=head1 ADDITION SCRIPTS
759
 
760
=item C<create_backup.sh>
761
 
762
Backup planner running from C<crontab>. For example: 
763
 
1154 dev 764
18 4 * * * /root/backup/fsbackup/create_backup.sh
1149 dev 765
 
766
=item C<install.pl>
767
 
768
Script to install fsbackup package and some required perl modules.
769
 
770
=item C<fsbackup.pl>
771
 
772
File system backup utility.
773
 
774
=item C<cfg_example>
775
 
776
Example of configuration file.
777
 
778
=item C<scripts/pgsql_backup.sh>
779
 
780
=item C<scripts/mysql_backup.sh>
781
 
782
Script for backup SQL tables from PostreSQL and MySQL.
783
 
784
=item C<scripts/sysbackup.sh>
785
 
786
Script for store system configuration files and information about installed
787
packages.
788
 
789
=item C<scripts/fsrestore.sh>
790
 
791
Script for restore files backuped by C<fsbackup.pl>.
792
 
793
=item C<scripts/sysrestore.sh>
794
 
795
Script for reinstall packages stored by C<sysbackup.sh>.
796
 
797
 
798
=head1 CONFIGURATION FILE
799
 
800
=item B<$cfg_backup_name> = 'test_host'
801
 
802
Name of backup, single word.
803
 
1154 dev 804
=item B<$cfg_cache_dir> = '/root/backup/fsbackup/cache'
1149 dev 805
 
806
Path of internal cache directory for local backup method.
807
 
808
=item B<$prog_md5sum> = 'md5sum -b'
809
 
810
=item B<$prog_tar> = 'tar'
811
 
812
=item B<$prog_ssh> = 'ssh'
813
 
814
=item B<$prog_rm> = 'rm'
815
 
816
=item B<$prog_gzip> = 'gzip'
817
 
818
=item B<$prog_pgp> = 'gpg'
819
 
820
Full path of some external program running from C<fsbackup.pl>.
821
B<$prog_gzip = ''> - not use compression, B<$prog_pgp = ''> - not use 
822
encryption.
823
 
824
=item B<$cfg_checksum> = 'timesize'
825
 
826
File checksum method: 
827
 
828
timesize - checksum of file attributes (default, best speed) 
829
 
830
md5      - checksum of file attributes + MD5 checksum of file content.
831
 
832
=item B<$cfg_backup_style> = 'backup'
833
 
834
Backup style:
835
 
836
backup - incremental backup (copy only new and changed files).
837
 
838
full_backup - full backup (copy all files).	
839
 
840
sync - file tree synchronization.
841
 
842
hash - hash creation without storing archive (spying for new or changed files).
843
 
844
=item B<$cfg_increment_level> = 7
845
 
846
Incremental level (after how many incremental copy make full refresh of backup)
847
 
848
=item B<$cfg_type> = 'remote_ssh'
849
 
850
Type of backup storage:
851
 
852
    local  - store backup on local file system.
853
    remote_ssh - store backup on remote host over SSH connection.
854
    remote_ftp - store backup on remote FTP server.
855
 
856
 
857
=item B<$cfg_remote_host> = 'backup-server.test.ru'
858
 
859
=item B<$cfg_remote_login> = 'backup_login'
860
 
861
=item B<$cfg_remote_path> = '/home/backup_login/backup'
862
 
863
Connection parameters for remote_ssh storage type.
864
 
865
=item B<$cfg_remote_password> = 'Test1234'
866
 
867
Password of remote login for remote_ftp storage type.
868
 
869
=item B<$cfg_local_path> = '/var/backup/'
870
 
871
Path of directory to store backup on local file system for local storage type.
872
 
873
=item B<$cfg_time_limit> = 0
874
 
875
Limit of file creation time in days. If not 0, don't backup files created or 
876
modified later then $cfg_time_limit (days).
877
 
878
=item B<$cfg_size_limit> = 0
879
 
880
Limit of maximum file size. If not 0, don't backup files witch size more then 
881
$cfg_time_limit kilobytes.
882
 
883
=item B<$cfg_root_path> = '/'
884
 
885
Root path for initial chdir.
886
 
887
=item B<$cfg_pgp_userid> = ''
888
 
889
Name of user in public key ring with public key will be used for PGP encryption.
890
Not use encryption if not set.
891
 
892
=item B<$cfg_verbose> = 3
893
 
894
Verbose level.
895
 
896
 
897
    1	- Output errors and warnings.
898
    2	- Output all the  available  data.
899
 
900
=item B<$cfg_save_old_backup> = 1
901
 
902
Save previous backup to OLD directory before rotation or before storing full backup.
903
 
904
 
905
    1 - save old backup.
906
 
907
=item B<$cfg_maximum_archive_size> = 0
908
 
909
Size of maximum size (in KiloBytes) of single unpacked archive file (0 - unlimited file size).
910
 
911
=item B<$cfg_stopdir_prune> = 0
912
 
913
Recursive review of the prohibited directories.
914
 
915
    1 - not use a recursive entrance to directory prohibited for backup (speed is increased, reduces flexibility of customization).
916
 
917
=item B<__DATA__> - list of backuped path and regexp mask.
918
 
919
    /dir[/file] - backup file or directory.
920
    !/dir[/file] - NOT include this file or directory to backup.
921
    # - ignore this line.
922
 
923
Mask:
924
 
925
    =~ - regexp mask for include file or directory to backup.
926
    f~ - regexp file mask for include file to backup.
927
    d~ - regexp directory mask for include directory to backup.
928
    =! - regexp mask for NOT include file or directory to backup.
929
    f! - regexp file mask for NOT include file to backup.
930
    d! - regexp directory mask for NOT include directory to backup.
931
 
932
 
933
Operation priority:
934
 
935
    1. =!
936
    2. f!
937
    3. f~
938
    4. d!
939
    5. =~
940
    6. d~
941
    7. !path
942
    8. path
943
 
944
 
945
=head1 COPYRIGHT
946
 
947
Copyright (c) 2001 by Maxim Chirkov <mc@tyumen.ru>
948
http://www.opennet.ru/dev/fsbackup/
949
 
950
=head1 BUGS
951
 
952
Look TODO file.
953
 
954
=head1 AUTHORS
955
 
956
Maxim Chirkov <mc@tyumen.ru>
957
 
958
=cut