74 |
use POSIX qw/strftime/; |
use POSIX qw/strftime/; |
75 |
use File::Which; |
use File::Which; |
76 |
use File::Path; |
use File::Path; |
77 |
|
use File::Slurp; |
78 |
use Data::Dumper; ### FIXME |
use Data::Dumper; ### FIXME |
79 |
|
|
80 |
die("BackupPC::Lib->new failed\n") if ( !(my $bpc = BackupPC::Lib->new) ); |
die("BackupPC::Lib->new failed\n") if ( !(my $bpc = BackupPC::Lib->new) ); |
86 |
my $in_backup_increment; |
my $in_backup_increment; |
87 |
|
|
88 |
|
|
89 |
if ( !getopts("th:n:p:r:s:b:w:vd", \%opts) ) { |
if ( !getopts("th:n:p:r:s:b:w:vdf", \%opts) ) { |
90 |
print STDERR <<EOF; |
print STDERR <<EOF; |
91 |
usage: $0 [options] |
usage: $0 [options] |
92 |
Required options: |
Required options: |
102 |
-p pathAdd new path prefix |
-p pathAdd new path prefix |
103 |
-b BLOCKS BLOCKS x 512 bytes per record (default 20; same as tar) |
-b BLOCKS BLOCKS x 512 bytes per record (default 20; same as tar) |
104 |
-w writeBufSz write buffer size (default 1048576 = 1MB) |
-w writeBufSz write buffer size (default 1048576 = 1MB) |
105 |
|
-f overwrite existing parts |
106 |
-v verbose output |
-v verbose output |
107 |
-d debug output |
-d debug output |
108 |
EOF |
EOF |
110 |
} |
} |
111 |
|
|
112 |
if ( $opts{h} !~ /^([\w\.\s-]+)$/ ) { |
if ( $opts{h} !~ /^([\w\.\s-]+)$/ ) { |
113 |
print(STDERR "$0: bad host name '$opts{h}'\n"); |
die "$0: bad host name '$opts{h}'\n"; |
|
exit(1); |
|
114 |
} |
} |
115 |
my $Host = $opts{h}; |
my $Host = $opts{h}; |
116 |
|
|
117 |
if ( $opts{n} !~ /^(-?\d+)$/ ) { |
if ( $opts{n} !~ /^(-?\d+)$/ ) { |
118 |
print(STDERR "$0: bad dump number '$opts{n}'\n"); |
die "$0: bad dump number '$opts{n}'\n"; |
|
exit(1); |
|
119 |
} |
} |
120 |
my $Num = $opts{n}; |
my $Num = $opts{n}; |
121 |
|
|
131 |
my $SpecialCnt = 0; |
my $SpecialCnt = 0; |
132 |
my $ErrorCnt = 0; |
my $ErrorCnt = 0; |
133 |
my $current_tar_size = 0; |
my $current_tar_size = 0; |
134 |
|
my $total_increment_size = 0; |
135 |
|
|
136 |
my $i; |
my $i; |
137 |
$Num = $Backups[@Backups + $Num]{num} if ( -@Backups <= $Num && $Num < 0 ); |
$Num = $Backups[@Backups + $Num]{num} if ( -@Backups <= $Num && $Num < 0 ); |
139 |
last if ( $Backups[$i]{num} == $Num ); |
last if ( $Backups[$i]{num} == $Num ); |
140 |
} |
} |
141 |
if ( $i >= @Backups ) { |
if ( $i >= @Backups ) { |
142 |
print(STDERR "$0: bad backup number $Num for host $Host\n"); |
die "$0: bad backup number $Num for host $Host\n"; |
|
exit(1); |
|
143 |
} |
} |
144 |
|
|
145 |
my $PathRemove = $1 if ( $opts{r} =~ /(.+)/ ); |
my $PathRemove = $1 if ( $opts{r} =~ /(.+)/ ); |
146 |
my $PathAdd = $1 if ( $opts{p} =~ /(.+)/ ); |
my $PathAdd = $1 if ( $opts{p} =~ /(.+)/ ); |
147 |
if ( $opts{s} !~ /^([\w\s\.\/\$-]+)$/ && $opts{s} ne "*" ) { |
if ( $opts{s} !~ /^([\w\s\.\/\$-]+)$/ && $opts{s} ne "*" ) { |
148 |
print(STDERR "$0: bad share name '$opts{s}'\n"); |
die "$0: bad share name '$opts{s}'\n"; |
|
exit(1); |
|
149 |
} |
} |
150 |
our $ShareName = $opts{s}; |
our $ShareName = $opts{s}; |
151 |
our $view = BackupPC::View->new($bpc, $Host, \@Backups); |
our $view = BackupPC::View->new($bpc, $Host, \@Backups); |
152 |
|
|
153 |
|
# database |
154 |
|
|
155 |
|
my $dsn = $Conf{SearchDSN}; |
156 |
|
my $db_user = $Conf{SearchUser} || ''; |
157 |
|
|
158 |
|
my $dbh = DBI->connect($dsn, $db_user, "", { RaiseError => 1, AutoCommit => 0} ); |
159 |
|
|
160 |
|
my $sth_inc_size = $dbh->prepare(qq{ |
161 |
|
update backups set |
162 |
|
inc_size = ?, |
163 |
|
inc_deleted = false |
164 |
|
where id = ? |
165 |
|
}); |
166 |
|
my $sth_backup_parts = $dbh->prepare(qq{ |
167 |
|
insert into backup_parts ( |
168 |
|
backup_id, |
169 |
|
part_nr, |
170 |
|
tar_size, |
171 |
|
size, |
172 |
|
md5, |
173 |
|
items |
174 |
|
) values (?,?,?,?,?,?) |
175 |
|
}); |
176 |
|
|
177 |
# |
# |
178 |
# This constant and the line of code below that uses it are borrowed |
# This constant and the line of code below that uses it are borrowed |
179 |
# from Archive::Tar. Thanks to Calle Dybedahl and Stephen Zander. |
# from Archive::Tar. Thanks to Calle Dybedahl and Stephen Zander. |
198 |
# |
# |
199 |
|
|
200 |
my $max_file_size = $Conf{'MaxArchiveFileSize'} || die "problem with MaxArchiveFileSize parametar"; |
my $max_file_size = $Conf{'MaxArchiveFileSize'} || die "problem with MaxArchiveFileSize parametar"; |
|
$max_file_size *= 1024; |
|
201 |
|
|
202 |
my $tar_dir = $Conf{InstallDir}.'/'.$Conf{GzipTempDir}; |
my $tar_dir = $Conf{InstallDir}.'/'.$Conf{GzipTempDir}; |
203 |
die "problem with $tar_dir, check GzipTempDir in configuration\n" unless (-d $tar_dir && -w $tar_dir); |
die "problem with $tar_dir, check GzipTempDir in configuration\n" unless (-d $tar_dir && -w $tar_dir); |
204 |
|
|
205 |
my $tar_file = BackupPC::SearchLib::getGzipName($Host, $ShareName, $Num) || die "can't getGzipName($Host, $ShareName, $Num)"; |
my $tar_file = BackupPC::SearchLib::getGzipName($Host, $ShareName, $Num) || die "can't getGzipName($Host, $ShareName, $Num)"; |
206 |
|
|
207 |
my $tar_path = $tar_dir . '/' . $tar_file . '.tmp'; |
my $tar_path_final = $tar_dir . '/' . $tar_file; |
208 |
|
my $tar_path = $tar_path_final . '.tmp'; |
209 |
|
|
210 |
$tar_path =~ s#//#/#g; |
$tar_path =~ s#//#/#g; |
211 |
|
|
212 |
print STDERR "working dir: $tar_dir, max uncompressed size $max_file_size bytes, tar $tar_file\n" if ($opts{d}); |
my $sth = $dbh->prepare(qq{ |
213 |
|
SELECT |
214 |
|
backups.id |
215 |
|
FROM backups |
216 |
|
JOIN shares on shares.id = shareid |
217 |
|
JOIN hosts on hosts.id = shares.hostid |
218 |
|
WHERE hosts.name = ? and shares.name = ? and backups.num = ? |
219 |
|
}); |
220 |
|
$sth->execute($Host, $ShareName, $Num); |
221 |
|
my ($backup_id) = $sth->fetchrow_array; |
222 |
|
$sth->finish; |
223 |
|
|
224 |
|
|
225 |
|
# delete exising backup_parts |
226 |
|
my $sth_delete_backup_parts = $dbh->prepare(qq{ |
227 |
|
delete from backup_parts |
228 |
|
where backup_id = ? |
229 |
|
}); |
230 |
|
$sth_delete_backup_parts->execute($backup_id); |
231 |
|
|
232 |
|
|
233 |
|
print STDERR "backup_id: $backup_id working dir: $tar_dir, max uncompressed size $max_file_size bytes, tar $tar_file\n" if ($opts{d}); |
234 |
|
|
235 |
|
if (-e $tar_path_final) { |
236 |
|
if ($opts{f}) { |
237 |
|
rmtree $tar_path_final || die "can't remove $tar_path_final: $!"; |
238 |
|
} else { |
239 |
|
die "$tar_path_final allready exists\n"; |
240 |
|
} |
241 |
|
} |
242 |
|
|
243 |
my $fh; |
my $fh; |
244 |
my $part = 0; |
my $part = 0; |
245 |
my $no_files = 0; |
my $no_files = 0; |
246 |
|
my $items_in_part = 0; |
247 |
|
|
248 |
sub new_tar_part { |
sub new_tar_part { |
249 |
|
my $arg = {@_}; |
250 |
|
|
251 |
if ($fh) { |
if ($fh) { |
252 |
return if ($current_tar_size == 0); |
return if ($current_tar_size == 0); |
253 |
|
|
254 |
print STDERR "# closing part $part\n" if ($opts{d}); |
print STDERR "\n\t+ $part:"; |
255 |
|
|
256 |
# finish tar archive |
# |
257 |
|
# Finish with two null 512 byte headers, |
258 |
|
# and then round out a full block. |
259 |
|
# |
260 |
my $data = "\0" x ($tar_header_length * 2); |
my $data = "\0" x ($tar_header_length * 2); |
261 |
TarWrite($fh, \$data); |
TarWrite($fh, \$data); |
262 |
TarWrite($fh, undef); |
TarWrite($fh, undef); |
263 |
|
|
264 |
close($fh) || die "can't close archive part $part: $!"; |
close($fh) || die "can't close archive part $part: $!"; |
265 |
|
|
266 |
|
my $file = $tar_path . '/' . $part; |
267 |
|
|
268 |
|
my $md5 = read_file( $file . '.md5' ) || die "can't read md5sum file ${file}.md5"; |
269 |
|
$md5 =~ s/\s.*$//; |
270 |
|
|
271 |
|
my $size = (stat( $file . '.tar.gz' ))[7] || die "can't stat ${file}.tar.gz"; |
272 |
|
|
273 |
|
print "$file, $size bytes, $items_in_part items"; |
274 |
|
|
275 |
|
$sth_backup_parts->execute( |
276 |
|
$backup_id, |
277 |
|
$part, |
278 |
|
$current_tar_size, |
279 |
|
$size, |
280 |
|
$md5, |
281 |
|
$items_in_part, |
282 |
|
); |
283 |
|
|
284 |
|
$total_increment_size += $size; |
285 |
|
|
286 |
|
if ($arg->{close}) { |
287 |
|
|
288 |
|
sub move($$) { |
289 |
|
my ($from,$to) = @_; |
290 |
|
print STDERR "# rename $from -> $to\n" if ($opts{d}); |
291 |
|
rename $from, $to || die "can't move $from -> $to: $!\n"; |
292 |
|
} |
293 |
|
|
294 |
|
if ($part == 1) { |
295 |
|
print STDERR " single" if ($opts{v}); |
296 |
|
move("${tar_path}/1.tar.gz", "${tar_path_final}.tar.gz"); |
297 |
|
move("${tar_path}/1.md5", "${tar_path_final}.md5"); |
298 |
|
rmtree $tar_path or die "can't remove temporary dir $tar_path: $!"; |
299 |
|
} else { |
300 |
|
print STDERR " [last]" if ($opts{v}); |
301 |
|
move("${tar_path}", "${tar_path_final}"); |
302 |
|
|
303 |
|
# if this archive was single part, remove it |
304 |
|
foreach my $suffix (qw/.tar.gz .md5/) { |
305 |
|
my $path = $tar_path_final . $suffix; |
306 |
|
unlink $path if (-e $path); |
307 |
|
} |
308 |
|
} |
309 |
|
|
310 |
|
$sth_inc_size->execute( |
311 |
|
$total_increment_size, |
312 |
|
$backup_id |
313 |
|
); |
314 |
|
|
315 |
|
print "\n\ttotal $total_increment_size bytes"; |
316 |
|
|
317 |
|
return; |
318 |
|
} |
319 |
|
|
320 |
} |
} |
321 |
|
|
322 |
$part++; |
$part++; |
324 |
# if this is first part, create directory |
# if this is first part, create directory |
325 |
|
|
326 |
if ($part == 1) { |
if ($part == 1) { |
327 |
if (-d $tar_path) { |
if (-e $tar_path) { |
328 |
print STDERR "# deleting existing $tar_path\n" if ($opts{d}); |
print STDERR "# deleting existing $tar_path\n" if ($opts{d}); |
329 |
rmtree($tar_path); |
rmtree($tar_path); |
330 |
} |
} |
331 |
mkdir($tar_path) || die "can't create directory $tar_path: $!"; |
mkdir($tar_path) || die "can't create directory $tar_path: $!"; |
332 |
|
|
333 |
|
sub abort_cleanup { |
334 |
|
print STDERR "ABORTED: cleanup temp dir "; |
335 |
|
rmtree($tar_path); |
336 |
|
$dbh->rollback; |
337 |
|
exit 1; |
338 |
|
} |
339 |
|
|
340 |
|
$SIG{'INT'} = \&abort_cleanup; |
341 |
|
$SIG{'QUIT'} = \&abort_cleanup; |
342 |
|
$SIG{'__DIE__'} = \&abort_cleanup; |
343 |
|
|
344 |
} |
} |
345 |
|
|
346 |
my $file = $tar_path . '/' . $part; |
my $file = $tar_path . '/' . $part; |
359 |
|
|
360 |
open($fh, $cmd) or die "can't open $cmd: $!"; |
open($fh, $cmd) or die "can't open $cmd: $!"; |
361 |
binmode($fh); |
binmode($fh); |
362 |
|
|
363 |
$current_tar_size = 0; |
$current_tar_size = 0; |
364 |
|
$items_in_part = 0; |
365 |
} |
} |
366 |
|
|
367 |
new_tar_part(); |
new_tar_part(); |
369 |
if (seedCache($Host, $ShareName, $Num)) { |
if (seedCache($Host, $ShareName, $Num)) { |
370 |
archiveWrite($fh, '/'); |
archiveWrite($fh, '/'); |
371 |
archiveWriteHardLinks($fh); |
archiveWriteHardLinks($fh); |
372 |
|
new_tar_part( close => 1 ); |
373 |
} else { |
} else { |
374 |
print STDERR "NOTE: no files found for $Host:$ShareName, increment $Num\n" if ($opts{v}); |
print STDERR "NOTE: no files found for $Host:$ShareName, increment $Num\n" if ($opts{v}); |
375 |
$no_files = 1; |
# remove temporary files if there are no files |
|
} |
|
|
|
|
|
# |
|
|
# Finish with two null 512 byte headers, and then round out a full |
|
|
# block. |
|
|
# |
|
|
my $data = "\0" x ($tar_header_length * 2); |
|
|
TarWrite($fh, \$data); |
|
|
TarWrite($fh, undef); |
|
|
|
|
|
if (! close($fh)) { |
|
376 |
rmtree($tar_path); |
rmtree($tar_path); |
|
die "can't close archive\n"; |
|
|
} |
|
377 |
|
|
378 |
# remove temporary files if there are no files |
my $sth = $dbh->prepare(qq{ |
379 |
if ($no_files) { |
update backups set inc_size = 0, inc_deleted = true |
380 |
rmtree($tar_path); |
where id = ? |
381 |
} elsif ($part == 1) { |
}); |
382 |
warn "FIXME: if there is only one part move to parent directory and rename"; |
$sth->execute($backup_id); |
383 |
|
|
384 |
} |
} |
385 |
|
|
386 |
# |
# |
395 |
# Got errors, with no files or directories; exit with non-zero |
# Got errors, with no files or directories; exit with non-zero |
396 |
# status |
# status |
397 |
# |
# |
398 |
cleanup(); |
die "got errors or no files\n"; |
|
exit(1); |
|
399 |
} |
} |
400 |
|
|
401 |
exit(0); |
$sth_inc_size->finish; |
402 |
|
$sth_backup_parts->finish; |
403 |
|
|
404 |
|
$dbh->commit || die "can't commit changes to database"; |
405 |
|
$dbh->disconnect(); |
406 |
|
|
407 |
|
exit; |
408 |
|
|
409 |
########################################################################### |
########################################################################### |
410 |
# Subroutines |
# Subroutines |
491 |
my $done = $WriteBufSz - length($WriteBuf); |
my $done = $WriteBufSz - length($WriteBuf); |
492 |
if ( syswrite($fh, $WriteBuf . substr($$dataRef, 0, $done)) |
if ( syswrite($fh, $WriteBuf . substr($$dataRef, 0, $done)) |
493 |
!= $WriteBufSz ) { |
!= $WriteBufSz ) { |
494 |
print(STDERR "Unable to write to output file ($!)\n"); |
die "Unable to write to output file ($!)\n"; |
|
exit(1); |
|
495 |
} |
} |
496 |
while ( $done + $WriteBufSz <= length($$dataRef) ) { |
while ( $done + $WriteBufSz <= length($$dataRef) ) { |
497 |
if ( syswrite($fh, substr($$dataRef, $done, $WriteBufSz)) |
if ( syswrite($fh, substr($$dataRef, $done, $WriteBufSz)) |
498 |
!= $WriteBufSz ) { |
!= $WriteBufSz ) { |
499 |
print(STDERR "Unable to write to output file ($!)\n"); |
die "Unable to write to output file ($!)\n"; |
|
exit(1); |
|
500 |
} |
} |
501 |
$done += $WriteBufSz; |
$done += $WriteBufSz; |
502 |
} |
} |
601 |
sub seedCache($$$) { |
sub seedCache($$$) { |
602 |
my ($host, $share, $dumpNo) = @_; |
my ($host, $share, $dumpNo) = @_; |
603 |
|
|
604 |
my $dsn = $Conf{SearchDSN}; |
print STDERR curr_time(), "$host:$share #$dumpNo" if ($opts{v}); |
|
my $db_user = $Conf{SearchUser} || ''; |
|
|
|
|
|
print STDERR curr_time(), "getting files for $host:$share increment $dumpNo..." if ($opts{v}); |
|
605 |
my $sql = q{ |
my $sql = q{ |
606 |
SELECT path,size |
SELECT path,size |
607 |
FROM files |
FROM files |
610 |
WHERE hosts.name = ? and shares.name = ? and backupnum = ? |
WHERE hosts.name = ? and shares.name = ? and backupnum = ? |
611 |
}; |
}; |
612 |
|
|
|
my $dbh = DBI->connect($dsn, $db_user, "", { RaiseError => 1, AutoCommit => 1} ); |
|
613 |
my $sth = $dbh->prepare($sql); |
my $sth = $dbh->prepare($sql); |
614 |
$sth->execute($host, $share, $dumpNo); |
$sth->execute($host, $share, $dumpNo); |
615 |
my $count = $sth->rows; |
my $count = $sth->rows; |
616 |
print STDERR " found $count items\n" if ($opts{v}); |
print STDERR " $count items, parts:" if ($opts{v}); |
617 |
while (my $row = $sth->fetchrow_arrayref) { |
while (my $row = $sth->fetchrow_arrayref) { |
618 |
#print STDERR "+ ", $row->[0],"\n"; |
#print STDERR "+ ", $row->[0],"\n"; |
619 |
$in_backup_increment->{ $row->[0] } = $row->[1]; |
$in_backup_increment->{ $row->[0] } = $row->[1]; |
620 |
} |
} |
621 |
|
|
622 |
$sth->finish(); |
$sth->finish(); |
|
$dbh->disconnect(); |
|
623 |
|
|
624 |
return $count; |
return $count; |
625 |
} |
} |
661 |
# is this file too large to fit into MaxArchiveFileSize? |
# is this file too large to fit into MaxArchiveFileSize? |
662 |
|
|
663 |
if ( ($current_tar_size + tar_overhead($tarPath) + $size) > $max_file_size ) { |
if ( ($current_tar_size + tar_overhead($tarPath) + $size) > $max_file_size ) { |
664 |
print STDERR "# tar file $current_tar_size + $tar_header_length + $size > $max_file_size, splitting\n" if ($opts{d}); |
print STDERR "# tar file $current_tar_size + $tar_header_length + $size > $max_file_size, splitting\n" if ($opts{d}); |
665 |
new_tar_part(); |
new_tar_part(); |
666 |
} |
} |
667 |
|
|
668 |
print STDERR "A $tarPath [$size] tell: $current_tar_size\n" if ($opts{d}); |
#print STDERR "A $tarPath [$size] tell: $current_tar_size\n" if ($opts{d}); |
669 |
|
$items_in_part++; |
670 |
|
|
671 |
if ( defined($PathRemove) |
if ( defined($PathRemove) |
672 |
&& substr($tarPath, 0, length($PathRemove)) eq $PathRemove ) { |
&& substr($tarPath, 0, length($PathRemove)) eq $PathRemove ) { |
739 |
TarWrite($fh, \$data); |
TarWrite($fh, \$data); |
740 |
$size += length($data); |
$size += length($data); |
741 |
TarWritePad($fh, $size); |
TarWritePad($fh, $size); |
742 |
|
|
743 |
|
$items_in_part++; |
744 |
} |
} |
745 |
$f->close; |
$f->close; |
746 |
$FileCnt++; |
$FileCnt++; |