* Added more options to server backup command: rather than just forcing
[BackupPC.git] / lib / BackupPC / Xfer / Ftp.pm
1 #============================================================= -*-perl-*-
2 #
3 # BackupPC::Xfer::Ftp package
4 #
5 # DESCRIPTION
6 #
7 #   This library defines a BackupPC::Xfer::Ftp class for transferring
8 #   data from a FTP client.
9 #
10 # AUTHOR
11 #   Paul Mantz <pcmantz@zmanda.com>
12 #
13 # COPYRIGHT
14 #   (C) 2008, Zmanda Inc.
15 #
16 #   This program is free software; you can redistribute it and/or
17 #   modify it under the terms of the GNU General Public License as
18 #   published by the Free Software Foundation; either version 2 of the
19 #   License, or (at your option) any later version.
20 #
21 #   This program is distributed in the hope that it will be useful,
22 #   but WITHOUT ANY WARRANTY; without even the implied warranty of
23 #   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
24 #   General Public License for more details.
25 #
26 #   You should have received a copy of the GNU General Public License
27 #   along with this program; if not, write to the Free Software
28 #   Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA
29 #   02111-1307 USA
30 #
31 #
32 #========================================================================
33 #
34 # Unreleased, planned release in 3.2 (or 3.1.1)
35 #
36 # See http://backuppc.sourceforge.net.
37 #
38 #========================================================================
39
40
41 package BackupPC::Xfer::Ftp;
42
43 use strict;
44
45 use BackupPC::View;
46 use BackupPC::Attrib qw(:all);
47
48 use Encode qw/from_to encode/;
49 use File::Listing qw/parse_dir/;
50 use File::Path;
51 use Data::Dumper;
52 use base qw(BackupPC::Xfer::Protocol);
53
54 use vars qw( $FTPLibOK $FTPLibErr $ARCLibOK );
55
56 use constant S_IFMT => 0170000;
57
58 BEGIN {
59
60     $FTPLibOK = 1;
61     $ARCLibOK = 0;
62
63     #
64     # clear eval error variable
65     #
66     my @FTPLibs = qw( Net::FTP Net::FTP::RetrHandle );
67
68     foreach my $module ( @FTPLibs ) {
69
70         undef $@;
71         eval "use $module;";
72
73         if ( $@ ) {
74             $FTPLibOK = 0;
75             $FTPLibErr = "module $module doesn't exist: $@";
76             last;
77         }
78     }
79
80     eval "use Net::FTP::AutoReconnect;";
81     $ARCLibOK = (defined($@)) ? 1 : 0;
82 };
83
84 ##############################################################################
85 # Constructor
86 ##############################################################################
87
88
89 #
90 #   usage:
91 #     $xfer = new BackupPC::Xfer::Ftp( $bpc, %args );
92 #
93 # new() is your default class constructor.  it also calls the
94 # constructor for Protocol as well.
95 #
96 sub new
97 {
98     my ( $class, $bpc, $args ) = @_;
99     $args ||= {};
100
101     my $t = BackupPC::Xfer::Protocol->new(
102         $bpc,
103         {
104            ftp   => undef,
105            stats => {
106                errorCnt          => 0,
107                TotalFileCnt      => 0,
108                TotalFileSize     => 0,
109                ExistFileCnt      => 0,
110                ExistFileSize     => 0,
111                ExistFileCompSize => 0,
112            },
113            %$args,
114         } );
115     return bless( $t, $class );
116 }
117
118
119 ##############################################################################
120 # Methods
121 ##############################################################################
122
123 #
124 #   usage:
125 #     $xfer->start();
126 #
127 # start() is called to configure and initiate a dump or restore,
128 # depending on the configured options.
129 #
130 sub start
131 {
132     my ($t) = @_;
133
134     my $bpc  = $t->{bpc};
135     my $conf = $t->{conf};
136
137     my ( @fileList, $logMsg, $incrDate, $args, $dumpText );
138
139     #
140     # initialize the statistics returned by getStats()
141     #
142     foreach ( qw/byteCnt fileCnt xferErrCnt xferBadShareCnt
143                  xferBadFileCnt xferOK hostAbort hostError
144                  lastOutputLine/ )
145     {
146         $t->{$_} = 0;
147     }
148
149     #
150     # Net::FTP::RetrHandle is necessary.
151     #
152     if ( !$FTPLibOK ) {
153         $t->{_errStr} = "Error: FTP transfer selected but module"
154           . " Net::FTP::RetrHandle is not installed.";
155         $t->{xferErrCnt}++;
156         return;
157     }
158
159     #
160     # standardize the file include/exclude settings if necessary
161     #
162     unless ( $t->{type} eq 'restore' ) {
163         $bpc->backupFileConfFix( $conf, "FtpShareName" );
164         $t->loadInclExclRegexps("FtpShareName");
165     }
166
167     #
168     # Convert the encoding type of the names if at all possible
169     #
170     from_to( $args->{shareName}, "utf8", $conf->{ClientCharset} )
171         if ( $conf->{ClientCharset} ne "" );
172
173     #
174     # Collect FTP configuration arguments and translate them for
175     # passing to the FTP module.
176     #
177     unless ( $args = $t->getFTPArgs() ) {
178         return;
179     }
180
181     #
182     # Create the Net::FTP::AutoReconnect or Net::FTP object.
183     #
184     undef $@;
185     eval {
186         $t->{ftp} = ($ARCLibOK) ? Net::FTP::AutoReconnect->new(%$args)
187                                 : Net::FTP->new(%$args);
188     };
189     if ($@) {
190         $t->{_errStr} = "Can't open connection to $args->{Host}: $!";
191         $t->{xferErrCnt}++;
192         return;
193     }
194
195     #
196     # Log in to the ftp server and set appropriate path information.
197     #
198     undef $@;
199     eval { $t->{ftp}->login( $conf->{FtpUserName}, $conf->{FtpPasswd} ); };
200     if ( $@ ) {
201         $t->{_errStr} = "Can't login to $args->{Host}: $!";
202         $t->{xferErrCnt}++;
203         return;
204     }
205
206     undef $@;
207     eval { $t->{ftp}->binary(); };
208     if ($@) {
209         $t->{_errStr} =
210           "Can't enable binary transfer mode to $args->{Host}: $!";
211         $t->{xferErrCnt}++;
212         return;
213     }
214
215     undef $@;
216     eval { $t->{shareName} =~ m/^\.?$/ || $t->{ftp}->cwd( $t->{shareName} ); };
217     if ($@) {
218         $t->{_errStr} =
219             "Can't change working directory to $t->{shareName}: $!";
220         $t->{xferErrCnt}++;
221         return;
222     }
223
224     undef $@;
225     eval { $t->{sharePath} = $t->{ftp}->pwd(); };
226     if ($@) {
227         $t->{_errStr} =
228             "Can't retrieve full working directory of $t->{shareName}: $!";
229         $t->{xferErrCnt}++;
230         return;
231     }
232
233     #
234     # log the beginning of action based on type
235     #
236     if ( $t->{type} eq 'restore' ) {
237         $logMsg = "restore started on directory $t->{shareName}";
238
239     } elsif ( $t->{type} eq 'full' ) {
240         $logMsg = "full backup started on directory $t->{shareName}";
241
242     } elsif ( $t->{type} eq 'incr' ) {
243
244         $incrDate = $bpc->timeStamp( $t->{incrBaseTime} - 3600, 1 );
245         $logMsg = "incremental backup started back to $incrDate" .
246             " (backup #$t->{incrBaseBkupNum}) for directory" . "
247             $t->{shareName}";
248     }
249
250     #
251     # call the recursive function based on the type of action
252     #
253     if ( $t->{type} eq 'restore' ) {
254
255         $t->restore();
256         $logMsg = "Restore of $args->{Host} complete";
257
258     } elsif ( $t->{type} eq 'incr' ) {
259
260         $t->backup();
261         $logMsg = "Incremental backup of $args->{Host} complete";
262
263     } elsif ( $t->{type} eq 'full' ) {
264
265         $t->backup();
266         $logMsg = "Full backup of $args->{Host} complete";
267     }
268
269     delete $t->{_errStr};
270     return $logMsg;
271 }
272
273
274 #
275 #
276 #
277 sub run
278 {
279     my ($t) = @_;
280     my $stats = $t->{stats};
281
282     my ( $tarErrs,      $nFilesExist, $sizeExist,
283          $sizeExistCom, $nFilesTotal, $sizeTotal );
284
285     #
286     # TODO: replace the $stats array with variables at the top level,
287     # ones returned by $getStats.  They should be identical.
288     #
289     $tarErrs      = 0;
290     $nFilesExist  = $stats->{ExistFileCnt};
291     $sizeExist    = $stats->{ExistFileSize};
292     $sizeExistCom = $stats->{ExistFileCompSize};
293     $nFilesTotal  = $stats->{TotalFileCnt};
294     $sizeTotal    = $stats->{TotalFileSize};
295
296     if ( $t->{type} eq "restore" ) {
297         return ( $t->{fileCnt}, $t->{byteCnt}, 0, 0 );
298
299     } else {
300         return \( $tarErrs,      $nFilesExist, $sizeExist,
301                   $sizeExistCom, $nFilesTotal, $sizeTotal );
302     }
303 }
304
305
306 #
307 #   usage:
308 #     $t->restore();
309 #
310 # TODO: finish or scuttle this function.  It is not necessary for a
311 # release.
312 #
313 sub restore
314 {
315     my $t = @_;
316
317     my $bpc = $t->{bpc};
318     my $fileList = $t->{fileList};
319
320     my ($path, $fileName, $fileAttr, $fileType );
321
322     #print STDERR "BackupPC::Xfer::Ftp->restore()";
323
324     #
325     # Prepare the view object
326     #
327     $t->{view} = BackupPC::View->new( $bpc, $t->{bkupSrcHost},
328                                       $t->{backups} );
329     my $view = $t->{view};
330
331   SCAN: foreach my $f ( @$fileList ) {
332
333         #print STDERR "restoring $f...\n";
334
335         $f =~ /(.*)\/([^\/]*)/;
336         $path     = $1;
337         $fileName = $2;
338
339         $view->dirCache($path);
340
341         $fileAttr = $view->fileAttrib($fileName);
342         $fileType = fileType2Text( $fileAttr->{type} );
343
344         if ( $fileType eq "dir") {
345             $t->restoreDir($fileName, $fileAttr);
346
347         } elsif ( $fileType eq "file" ) {
348             $t->restoreFile($fileName, $fileAttr);
349
350         } elsif ( $fileType eq "symlink" ) {
351             #
352             # ignore
353             #
354         } else {
355             #
356             # ignore
357             #
358         }
359     } # end SCAN
360 }
361
362
363 sub restoreDir
364 {
365     my ( $t, $dirName, $dirAttr ) = @_;
366
367     my $ftp    = $t->{ftp};
368     my $bpc    = $t->{bpc};
369     my $conf   = $t->{conf};
370     my $view   = $t->{view};
371     my $TopDir = $bpc->TopDir();
372
373     my $path    = "$dirAttr->{relPath}/$dirName";
374     my $dirList = $view->dirAttrib( -1, $t->{shareName}, $path );
375
376     my ( $fileName, $fileAttr, $fileType );
377
378     #print STDERR "BackupPC::Xfer::Ftp->restore($dirName)\n";
379
380     #
381     # Create the remote directory
382     #
383     undef $@;
384     eval { $ftp->mkdir( $path, 1 ); };
385     if ($@) {
386         $t->logFileAction( "fail", $dirName, $dirAttr );
387         return;
388     }
389
390  SCAN: while ( ($fileName, $fileAttr ) = each %$dirList ) {
391
392         $fileType = fileType2Text( $fileAttr->{type} );
393
394         if ( $fileType eq "dir" ) {
395             if ( $t->restoreDir( $fileName, $fileAttr ) ) {
396                 $t->logWrite( "restored: $path/$fileName\n", 5 );
397             } else {
398                 $t->logWrite( "restore failed: $path/$fileName\n", 3 );
399             }
400
401         } elsif ( $fileType eq "file" ) {
402             $t->restoreFile( $fileName, $fileAttr );
403
404         } elsif ( $fileType eq "hardlink" ) {
405             #
406             # Hardlinks cannot be restored.  however, if we have the
407             # target file in the pool, we can restore that.
408             #
409             $t->restoreFile( $fileName, $fileAttr );
410
411             next SCAN;
412
413         } elsif ( $fileType eq "symlink" ) {
414             #
415             # Symlinks cannot be restored
416             #
417             next SCAN;
418
419         } else {
420             #
421             # Ignore all other types (devices, doors, etc)
422             #
423             next SCAN;
424         }
425     }
426 }
427
428
429 sub restoreFile
430 {
431     my ($t, $fileName, $fileAttr ) = @_;
432
433     my $conf = $t->{conf};
434     my $ftp  = $t->{ftp};
435
436     my $poolFile = $fileAttr->{fullPath};
437     my $fileDest = ( $conf->{ClientCharset} ne "" )
438                  ? from_to( "$fileAttr->{relPath}/$fileName",
439                             "utf8", $conf->{ClientCharset} )
440                  : "$fileAttr->{relPath}/$fileName";
441
442     #print STDERR "BackupPC::Xfer::Ftp->restoreFile($fileName)\n";
443
444     undef $@;
445     eval {
446         if ( $ftp->put( $poolFile, $fileDest ) ) {
447             $t->logFileAction( "restore", $fileName, $fileAttr );
448         } else {
449             $t->logFileAction( "fail", $fileName, $fileAttr );
450         }
451     };
452     if ($@) {
453         $t->logFileAction( "fail", $fileName, $fileAttr );
454     }
455 }
456
457
458 #
459 #  usage:
460 #   $t->backup($path);
461 #
462 # $t->backup() is a recursive function that takes a path as an
463 # argument, and performs a backup on that folder consistent with the
464 # configuration parameters.  $path is considered rooted at
465 # $t->{shareName}, so no $ftp->cwd() command is necessary.
466 #
467 sub backup
468 {
469     my ($t) =  @_;
470
471     my $ftp    = $t->{ftp};
472     my $bpc    = $t->{bpc};
473     my $conf   = $t->{conf};
474     my $TopDir = $bpc->TopDir();
475     my $OutDir = "$TopDir/pc/$t->{client}/new/"
476       . $bpc->fileNameEltMangle( $t->{shareName} );
477
478     #
479     # Prepare the view object
480     #
481     $t->{view} = BackupPC::View->new( $bpc, $t->{client}, $t->{backups} );
482
483     #
484     # Prepare backup folder
485     #
486     unless ( eval { mkpath( $OutDir, 0, 0755 ); } ) {
487         $t->{_errStr} = "can't create OutDir: $OutDir";
488         $t->{xferErrCnt}++;
489         return;
490     }
491
492     #
493     # determine the filetype of the shareName and back it up
494     # appropriately.  For now, assume that $t->{shareName} is a
495     # directory.
496     #
497     my $f = {
498               relPath  => "",
499               fullName => $t->{shareName},
500             };
501
502     if ( $t->handleDir( $f, $OutDir ) ) {
503
504         $t->{xferOK} = 1;
505         return 1;
506
507     } else {
508
509         $t->{xferBadShareCnt}++;
510         return;
511     }
512 }
513
514
515 ####################################################################################
516 # FTP-specific functions
517 ####################################################################################
518
519
520 #
521 # This is an encapulation of the logic necessary to grab the arguments
522 # from %Conf and throw it in a hash pointer to be passed to the
523 # Net::FTP object.
524 #
525 sub getFTPArgs
526 {
527     my ($t)  = @_;
528     my $conf = $t->{conf};
529
530     return {
531         Host         => $conf->{ClientNameAlias}
532                      || $t->{hostIP}
533                      || $t->{host},
534         Firewall     => undef,                            # not used
535         FirewallType => undef,                            # not used
536         BlockSize    => $conf->{FtpBlockSize} || 10240,
537         Port         => $conf->{FtpPort}      || 21,
538         Timeout      => $conf->{FtpTimeout}   || 120,
539         Debug        => 0,                                # do not touch
540         Passive      => 1,                                # do not touch
541         Hash         => undef,                            # do not touch
542         LocalAddr    => "localhost",                      # do not touch
543     };
544 }
545
546
547 #
548 #   usage:
549 #     $dirList = $t->remotels($path);
550 #
551 # remotels() returns a reference to a list of hash references that
552 # describe the contents of each file in the directory of the path
553 # specified.
554 #
555 # In the future, I would like to make this function return objects in
556 # Attrib format.  That would be very optimal, and I could probably
557 # release the code to CPAN.
558 #
559 sub remotels
560 {
561     my ( $t, $path ) = @_;
562
563     my $ftp  = $t->{ftp};
564     my $bpc  = $t->{bpc};
565     my $conf = $t->{conf};
566
567     my ( $dirContents, $remoteDir, $f );
568
569     undef $@;
570     eval {
571         $dirContents = ( $path =~ /^\.?$/ ) ? $ftp->dir()
572                                             : $ftp->dir("$path/");
573     };
574     if ($@) {
575         $t->{xferErrCnt}++;
576         return "can't retrieve remote directory contents of $path: $!";
577     }
578
579     foreach my $info ( @{parse_dir($dirContents)} ) {
580
581         $f = {
582             name  => $info->[0],
583             type  => $info->[1],
584             size  => $info->[2],
585             mtime => $info->[3],
586             mode  => $info->[4],
587         };
588
589         $f->{utf8name} = $f->{name};
590         from_to( $f->{utf8name}, $conf->{ClientCharset}, "utf8" );
591
592         $f->{fullName} = "$t->{sharePath}/$path/$f->{name}";
593         $f->{fullName} =~ s/\/+/\//g;
594
595         $f->{relPath} = ($path eq "") ? $f->{name} : "$path/$f->{name}";
596         $f->{relPath} =~ s/\/+/\//g;
597
598         push( @$remoteDir, $f );
599     }
600     return $remoteDir;
601 }
602
603
604 #
605 # ignoreFileCheck() looks at the attributes of the arguments and the
606 # backup types, and determines if the file should be skipped in this
607 # backup.
608 #
609 sub ignoreFileCheck
610 {
611     my ( $t, $f, $attrib ) = @_;
612
613     if ( $f->{name} =~ /^\.\.?$/ ) {
614         return 1;
615     }
616
617     return ( !$t->checkIncludeExclude( $f->{fullName} ) );
618 }
619
620
621 #
622 # handleSymlink() backs up a symlink.
623 #
624 sub handleSymlink
625 {
626     my ( $t, $f, $OutDir, $attrib ) = @_;
627
628     my $conf = $t->{conf};
629     my $ftp  = $t->{ftp};
630     my ( $target, $targetDesc );
631
632     my $attribInfo = {
633         type  => BPC_FTYPE_SYMLINK,
634         mode  => $f->{mode},
635         uid   => undef,            # unsupported
636         gid   => undef,            # unsupported
637         size  => 0,
638         mtime => $f->{mtime},
639     };
640
641     #
642     # If we are following symlinks, back them up as the type of file
643     # they point to. Otherwise, backup the symlink.
644     #
645     if ( $conf->{FtpFollowSymlinks} ) {
646
647         #
648         # handle nested symlinks by recurring on the target until a
649         # file or directory is found.
650         #
651         $f->{type} =~ /^l (.*)/;
652         $target = $1;
653
654         undef $@;
655         eval {
656             if ( $targetDesc = $ftp->dir("$target/") ) {
657                 $t->handleSymDir( $f, $OutDir, $attrib, $targetDesc );
658
659             } elsif ( $targetDesc = $ftp->dir($target) ) {
660                 if ( $targetDesc->[4] eq 'file' ) {
661                     $t->handleSymFile( $f, $OutDir, $attrib );
662
663                 } elsif ( $targetDesc->[4] =~ /l (.*)/ ) {
664                     $t->logFileAction( "fail", $f->{utf8name}, $attribInfo );
665                     return;
666                 }
667             } else {
668                 $t->( "fail", $f );
669                 return;
670             }
671         };
672         if ($@) {
673             $t->logFileAction( "fail", $f->{utf8name}, $attribInfo );
674             return;
675         }
676
677     } else {
678         #
679         # If we are not following symlinks, record them normally.
680         #
681         $attrib->set( $f->{utf8name}, $attribInfo );
682         $t->logFileAction("create", $f->{utf8name}, $attribInfo);
683     }
684     return 1;
685 }
686
687
688 sub handleSymDir
689 {
690     my ($t, $fSym, $OutDir, $attrib, $targetDesc) = @_;
691
692     return 1;
693  }
694
695
696 sub handleSymFile
697 {
698     my ( $t, $fSym, $OutDir, $attrib, $targetDesc ) = @_;
699
700     my $bpc  = $t->{bpc};
701     my $conf = $t->{conf};
702
703     my $f = {
704         name  => $fSym->{name},
705         type  => $targetDesc->[1],
706         size  => $targetDesc->[2],
707         mtime => $targetDesc->[3],
708         mode  => $targetDesc->[4]
709     };
710
711     $f->{utf8name} = $fSym->{name};
712     from_to( $f->{utf8name}, $conf->{ClientCharset}, "utf8" );
713
714     $f->{relPath}  = $fSym->{relPath};
715     $f->{fullName} = "$t->{shareName}/$fSym->{relPath}/$fSym->{name}";
716     $f->{fullName} =~ s/\/+/\//g;
717
718     #
719     # since FTP servers follow symlinks, we can just do this:
720     #
721     return $t->handleFile( $f, $OutDir, $attrib );
722 }
723
724
725 #
726 # handleDir() backs up a directory, and initiates a backup of its
727 # contents.
728 #
729 sub handleDir
730 {
731     my ( $t, $dir, $OutDir ) = @_;
732
733     my $ftp   = $t->{ftp};
734     my $bpc   = $t->{bpc};
735     my $conf  = $t->{conf};
736     my $view  = $t->{view};
737     my $stats = $t->{stats};
738
739     my ( $exists, $digest, $outSize, $errs );
740     my ( $poolWrite, $poolFile, $attribInfo );
741     my ( $localDir, $remoteDir, $attrib, %expectedFiles );
742
743     if ( exists($dir->{utf8name})) {
744         $OutDir .= "/" . $bpc->fileNameMangle( $dir->{utf8name} );
745     }
746
747     unless ( -d $OutDir ) {
748
749         eval { mkpath( $OutDir, 0, 0755 ) };
750         if ( $@ ) {
751             $t->logFileAction( "fail", $dir->{utf8name}, $dir );
752             return;
753         } else {
754             $t->logFileAction( "create", $dir->{utf8name}, $dir );
755         }
756     }
757
758     $attrib    = BackupPC::Attrib->new( { compress => $t->{Compress} } );
759     $remoteDir = $t->remotels( $dir->{relPath} );
760
761     if ( $t->{type} eq "incr" ) {
762         $localDir  = $view->dirAttrib( $t->{incrBaseBkupNum},
763                                        $t->{shareName}, $dir->{relPath} );
764         %expectedFiles = map { $_ => 0 } sort keys %$localDir
765     }
766
767     #
768     # take care of each file in the directory
769     #
770  SCAN: foreach my $f ( @{$remoteDir} ) {
771
772         next SCAN if $t->ignoreFileCheck( $f, $attrib );
773
774         #
775         # handle based on filetype
776         #
777         if ( $f->{type} eq 'f' ) {
778             $t->handleFile( $f, $OutDir, $attrib );
779
780         } elsif ( $f->{type} eq 'd' ) {
781
782             $attribInfo = {
783                 type  => BPC_FTYPE_DIR,
784                 mode  => $f->{mode},
785                 uid   => undef,           # unsupported
786                 gid   => undef,           # unsupported
787                 size  => $f->{size},
788                 mtime => $f->{mtime},
789             };
790
791             #print STDERR "$f->{utf8name}: ". Dumper($attribInfo);
792
793             if ( $t->handleDir($f, $OutDir) ) {
794                 $attrib->set( $f->{utf8name}, $attribInfo);
795             }
796
797         } elsif ( $f->{type} =~ /^l (.*)/ ) {
798             $t->handleSymlink( $f, $OutDir, $attrib );
799
800         } else {
801             #
802             # do nothing
803             #
804         }
805
806         #
807         # Mark file as seen in expected files hash
808         #
809         $expectedFiles{ $f->{utf8name} }++ if ( $t->{type} eq "incr" );
810
811     } # end foreach (@{$remoteDir})
812
813     #
814     # If the backup type is incremental, mark the files that are not
815     # present on the server as deleted.
816     #
817     if ( $t->{type} eq "incr" ) {
818         while ( my ($f, $seen) = each %expectedFiles ) {
819             $attrib->set( $f, { type => BPC_FTYPE_DELETED } )
820                 unless ($seen);
821         }
822     }
823
824     #
825     # print the directory attributes, now that the directory is done.
826     #
827     my $fileName = $attrib->fileName($OutDir);
828     my $data     = $attrib->writeData();
829
830     $poolWrite = BackupPC::PoolWrite->new( $bpc, $fileName, length($data),
831                                            $t->{Compress} );
832     $poolWrite->write( \$data );
833     ( $exists, $digest, $outSize, $errs ) = $poolWrite->close();
834
835     #
836     # Explicit success
837     #
838     return 1;
839 }
840
841
842 #
843 # handleFile() backs up a file.
844 #
845 sub handleFile
846 {
847     my ( $t, $f, $OutDir, $attrib ) = @_;
848
849     my $bpc        = $t->{bpc};
850     my $ftp        = $t->{ftp};
851     my $view       = $t->{view};
852     my $stats      = $t->{stats};
853     my $newFilesFH = $t->{newFilesFH};
854
855     my ( $poolFile, $poolWrite, $data, $localSize );
856     my ( $exists, $digest, $outSize, $errs );
857     my ( $oldAttrib );
858     local *FTP;
859
860     #
861     # If this is an incremental backup and the file exists in a
862     # previous backup unchanged, write the attribInfo for the file
863     # accordingly.
864     #
865     if ( $t->{type} eq "incr" ) {
866         return 1 if $t->incrFileExistCheck( $f, $attrib );
867     }
868
869     my $attribInfo = {
870                        type  => BPC_FTYPE_FILE,
871                        mode  => $f->{mode},
872                        uid   => undef,            # unsupported
873                        gid   => undef,            # unsupported
874                        size  => $f->{size},
875                        mtime => $f->{mtime},
876                      };
877
878     #
879     # If this is a full backup or the file has changed on the host,
880     # back it up.
881     #
882     undef $@;
883     eval { tie ( *FTP, 'Net::FTP::RetrHandle', $ftp, $f->{fullName} ); };
884     if ( !*FTP || $@ ) {
885         $t->handleFileAction( "fail", $attribInfo );
886         $t->{xferBadFileCnt}++;
887         $stats->{errCnt}++;
888         return;
889     }
890
891     $poolFile  = $OutDir . "/" . $bpc->fileNameMangle( $f->{name} );
892     $poolWrite = BackupPC::PoolWrite->new( $bpc, $poolFile, $f->{size},
893                                            $bpc->{xfer}{compress} );
894
895     $localSize = 0;
896
897     undef $@;
898     eval {
899         while (<FTP>) {
900             $localSize += length($_);
901             $poolWrite->write( \$_ );
902         }
903     };
904     ( $exists, $digest, $outSize, $errs ) = $poolWrite->close();
905     if ( !*FTP || $@ || @$errs ) {
906
907         $t->logFileAction( "fail", $f->{utf8name}, $attribInfo );
908         unlink($poolFile);
909         $t->{xferBadFileCnt}++;
910         $stats->{errCnt} += scalar @$errs;
911         return;
912     }
913
914     #
915     # this should never happen
916     #
917     if ( $localSize != $f->{size} ) {
918         $t->logFileAction( "fail", $f->{utf8name}, $attribInfo );
919         unklink($poolFile);
920         $stats->{xferBadFileCnt}++;
921         $stats->{errCnt}++;
922         return;
923     }
924
925     #
926     # Perform logging
927     #
928     $attrib->set( $f->{utf8name}, $attribInfo );
929     $t->logFileAction( $exists ? "pool" : "create",
930                        $f->{utf8name}, $attribInfo );
931     print $newFilesFH "$digest $f->{size} $poolFile\n" unless $exists;
932
933     #
934     # Cumulate the stats
935     #
936     $stats->{TotalFileCnt}++;
937     $stats->{ExistFileCnt}++;
938     $stats->{ExistFileCompSize} += -s $poolFile;
939     $stats->{ExistFileSize}     += $f->{size};
940     $stats->{TotalFileSize}     += $f->{size};
941
942     $t->{byteCnt} += $localSize;
943     $t->{fileCnt}++;
944 }
945
946
947 #
948 # this function checks if the file has been modified on disk, and if
949 # it has, returns.  Otherwise, it updates the attrib values.
950 #
951 sub incrFileExistCheck
952 {
953     my ($t, $f, $attrib) = @_;
954
955     my $view = $t->{view};
956
957     my $oldAttribInfo = $view->fileAttrib( $t->{incrBaseBkupNum},
958                                        $t->{shareName}, $f->{relPath} );
959
960     #print STDERR "*" x 50 . "\n";
961     #print STDERR "Old data:\n" . Dumper($oldAttribInfo);
962     #print STDERR "New data:\n" . Dumper($f);
963     #print STDERR "$f->{fullName}: $oldAttribInfo->{mtime} ?= $f->{mtime}, $oldAttribInfo->{size} ?= $f->{size}\n";
964
965     return ( $oldAttribInfo->{mtime} == $f->{mtime}
966           && $oldAttribInfo->{size} == $f->{size} );
967 }
968
969
970 #
971 # Generate a log file message for a completed file.  Taken from
972 # BackupPC_tarExtract. $f should be an attrib object.
973 #
974 sub logFileAction
975 {
976     my ( $t, $action, $name, $attrib ) = @_;
977
978     my $owner = "$attrib->{uid}/$attrib->{gid}";
979     my $type =
980       ( ( "", "p", "c", "", "d", "", "b", "", "", "", "l", "", "s" ) )
981       [ ( $attrib->{mode} & S_IFMT ) >> 12 ];
982
983     $name  = "."   if ( $name  eq "" );
984     $owner = "-/-" if ( $owner eq "/" );
985
986     my $fileAction = sprintf( "  %-6s %1s%4o %9s %11.0f %s\n",
987                               $action, $type, $attrib->{mode} & 07777,
988                               $owner, $attrib->{size}, $name );
989
990     return $t->logWrite( $fileAction, 1 );
991 }
992
993 1;