1 #============================================================= -*-perl-*-
3 # BackupPC::Xfer::Ftp package
7 # This library defines a BackupPC::Xfer::Ftp class for transferring
8 # data from a FTP client.
11 # Paul Mantz <pcmantz@zmanda.com>
14 # (C) 2008, Zmanda Inc.
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.
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.
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
32 #========================================================================
34 # Unreleased, planned release in 3.2 (or 3.1.1)
36 # See http://backuppc.sourceforge.net.
38 #========================================================================
41 package BackupPC::Xfer::Ftp;
46 use BackupPC::Attrib qw(:all);
48 use Encode qw/from_to encode/;
49 use File::Listing qw/parse_dir/;
52 use base qw(BackupPC::Xfer::Protocol);
54 use vars qw( $FTPLibOK $FTPLibErr $ARCLibOK );
56 use constant S_IFMT => 0170000;
64 # clear eval error variable
66 my @FTPLibs = qw( Net::FTP Net::FTP::RetrHandle );
68 foreach my $module ( @FTPLibs ) {
75 $FTPLibErr = "module $module doesn't exist: $@";
80 eval "use Net::FTP::AutoReconnect;";
81 $ARCLibOK = (defined($@)) ? 1 : 0;
84 ##############################################################################
86 ##############################################################################
91 # $xfer = new BackupPC::Xfer::Ftp( $bpc, %args );
93 # new() is your default class constructor. it also calls the
94 # constructor for Protocol as well.
98 my ( $class, $bpc, $args ) = @_;
101 my $t = BackupPC::Xfer::Protocol->new(
111 ExistFileCompSize => 0,
115 return bless( $t, $class );
119 ##############################################################################
121 ##############################################################################
127 # start() is called to configure and initiate a dump or restore,
128 # depending on the configured options.
135 my $conf = $t->{conf};
137 my ( @fileList, $logMsg, $incrDate, $args, $dumpText );
140 # initialize the statistics returned by getStats()
142 foreach ( qw/byteCnt fileCnt xferErrCnt xferBadShareCnt
143 xferBadFileCnt xferOK hostAbort hostError
150 # Net::FTP::RetrHandle is necessary.
153 $t->{_errStr} = "Error: FTP transfer selected but module"
154 . " Net::FTP::RetrHandle is not installed.";
160 # standardize the file include/exclude settings if necessary
162 unless ( $t->{type} eq 'restore' ) {
163 $bpc->backupFileConfFix( $conf, "FtpShareName" );
164 $t->loadInclExclRegexps("FtpShareName");
168 # Convert the encoding type of the names if at all possible
170 from_to( $args->{shareName}, "utf8", $conf->{ClientCharset} )
171 if ( $conf->{ClientCharset} ne "" );
174 # Collect FTP configuration arguments and translate them for
175 # passing to the FTP module.
177 $args = $t->getFTPArgs();
180 # Create the Net::FTP::AutoReconnect or Net::FTP object.
182 unless ( $t->{ftp} = ($ARCLibOK) ? Net::FTP::AutoReconnect->new(%$args)
183 : Net::FTP->new(%$args) )
185 $t->{_errStr} = "Can't open connection to $args->{Host}";
191 # Log in to the ftp server and set appropriate path information.
193 unless ( $t->{ftp}->login( $conf->{FtpUserName}, $conf->{FtpPasswd} ) ) {
194 $t->{_errStr} = "Can't login to $args->{Host}";
199 unless ( $t->{ftp}->binary() ) {
200 $t->{_errStr} = "Can't enable binary transfer mode to $args->{Host}";
205 unless ( ( $t->{shareName} =~ m/^\.?$/ )
206 || ( $t->{ftp}->cwd( $t->{shareName} ) ) )
208 $t->{_errStr} = "Can't change working directory to $t->{shareName}";
213 unless ( $t->{sharePath} = $t->{ftp}->pwd() ) {
214 $t->{_errStr} = "Can't retrieve full working directory of $t->{shareName}";
220 # log the beginning of action based on type
222 if ( $t->{type} eq 'restore' ) {
223 $logMsg = "restore started on directory $t->{shareName}";
225 } elsif ( $t->{type} eq 'full' ) {
226 $logMsg = "full backup started on directory $t->{shareName}";
228 } elsif ( $t->{type} eq 'incr' ) {
230 $incrDate = $bpc->timeStamp( $t->{incrBaseTime} - 3600, 1 );
231 $logMsg = "incremental backup started back to $incrDate" .
232 " (backup #$t->{incrBaseBkupNum}) for directory" . "
237 # call the recursive function based on the type of action
239 if ( $t->{type} eq 'restore' ) {
242 $logMsg = "Restore of $args->{Host} complete";
244 } elsif ( $t->{type} eq 'incr' ) {
247 $logMsg = "Incremental backup of $args->{Host} complete";
249 } elsif ( $t->{type} eq 'full' ) {
252 $logMsg = "Full backup of $args->{Host} complete";
255 delete $t->{_errStr};
266 my $stats = $t->{stats};
268 my ( $tarErrs, $nFilesExist, $sizeExist,
269 $sizeExistCom, $nFilesTotal, $sizeTotal );
272 # TODO: replace the $stats array with variables at the top level,
273 # ones returned by $getStats. They should be identical.
276 $nFilesExist = $stats->{ExistFileCnt};
277 $sizeExist = $stats->{ExistFileSize};
278 $sizeExistCom = $stats->{ExistFileCompSize};
279 $nFilesTotal = $stats->{TotalFileCnt};
280 $sizeTotal = $stats->{TotalFileSize};
282 if ( $t->{type} eq "restore" ) {
283 return ( $t->{fileCnt}, $t->{byteCnt}, 0, 0 );
286 return \( $tarErrs, $nFilesExist, $sizeExist,
287 $sizeExistCom, $nFilesTotal, $sizeTotal );
296 # TODO: finish or scuttle this function. It is not necessary for a
304 my $fileList = $t->{fileList};
306 my ($path, $fileName, $fileAttr, $fileType );
308 #print STDERR "BackupPC::Xfer::Ftp->restore()";
311 # Prepare the view object
313 $t->{view} = BackupPC::View->new( $bpc, $t->{bkupSrcHost},
315 my $view = $t->{view};
317 SCAN: foreach my $f ( @$fileList ) {
319 #print STDERR "restoring $f...\n";
321 $f =~ /(.*)\/([^\/]*)/;
325 $view->dirCache($path);
327 $fileAttr = $view->fileAttrib($fileName);
328 $fileType = fileType2Text( $fileAttr->{type} );
330 if ( $fileType eq "dir") {
331 $t->restoreDir($fileName, $fileAttr);
333 } elsif ( $fileType eq "file" ) {
334 $t->restoreFile($fileName, $fileAttr);
336 } elsif ( $fileType eq "symlink" ) {
351 my ( $t, $dirName, $dirAttr ) = @_;
355 my $conf = $t->{conf};
356 my $view = $t->{view};
357 my $TopDir = $bpc->TopDir();
359 my $path = "$dirAttr->{relPath}/$dirName";
360 my $dirList = $view->dirAttrib( -1, $t->{shareName}, $path );
362 my ( $fileName, $fileAttr, $fileType );
364 #print STDERR "BackupPC::Xfer::Ftp->restore($dirName)\n";
367 # Create the remote directory
369 unless ( $ftp->mkdir( $path, 1 ) ) {
371 $t->logFileAction( "fail", $dirName, $dirAttr );
375 SCAN: while ( ($fileName, $fileAttr ) = each %$dirList ) {
377 $fileType = fileType2Text( $fileAttr->{type} );
379 if ( $fileType eq "dir" ) {
380 if ( $t->restoreDir( $fileName, $fileAttr ) ) {
381 $t->logWrite( "restored: $path/$fileName\n", 5 );
383 $t->logWrite( "restore failed: $path/$fileName\n", 3 );
386 } elsif ( $fileType eq "file" ) {
387 $t->restoreFile( $fileName, $fileAttr );
389 } elsif ( $fileType eq "hardlink" ) {
391 # Hardlinks cannot be restored. however, if we have the
392 # target file in the pool, we can restore that.
394 $t->restoreFile( $fileName, $fileAttr );
398 } elsif ( $fileType eq "symlink" ) {
400 # Symlinks cannot be restored
406 # Ignore all other types (devices, doors, etc)
416 my ($t, $fileName, $fileAttr ) = @_;
418 my $conf = $t->{conf};
421 my $poolFile = $fileAttr->{fullPath};
422 my $fileDest = ( $conf->{ClientCharset} ne "" )
423 ? from_to( "$fileAttr->{relPath}/$fileName",
424 "utf8", $conf->{ClientCharset} )
425 : "$fileAttr->{relPath}/$fileName";
427 #print STDERR "BackupPC::Xfer::Ftp->restoreFile($fileName)\n";
430 # Note: is logging necessary here?
432 if ( $ftp->put( $poolFile, $fileDest ) ) {
433 $t->logFileAction("restore", $fileName, $fileAttr);
436 $t->logFileAction("fail", $fileName, $fileAttr);
445 # $t->backup() is a recursive function that takes a path as an
446 # argument, and performs a backup on that folder consistent with the
447 # configuration parameters. $path is considered rooted at
448 # $t->{shareName}, so no $ftp->cwd() command is necessary.
456 my $conf = $t->{conf};
457 my $TopDir = $bpc->TopDir();
458 my $OutDir = "$TopDir/pc/$t->{client}/new/"
459 . $bpc->fileNameEltMangle( $t->{shareName} );
462 # Prepare the view object
464 $t->{view} = BackupPC::View->new( $bpc, $t->{client}, $t->{backups} );
467 # Prepare backup folder
469 unless ( mkpath( $OutDir, 0, 0755 ) ) {
470 $t->{_errStr} = "can't create OutDir: $OutDir";
476 # determine the filetype of the shareName and back it up
477 # appropriately. For now, assume that $t->{shareName} is a
482 fullName => $t->{shareName},
485 if ( $t->handleDir( $f, $OutDir ) ) {
492 $t->{xferBadShareCnt}++;
498 ####################################################################################
499 # FTP-specific functions
500 ####################################################################################
504 # This is an encapulation of the logic necessary to grab the arguments
505 # from %Conf and throw it in a hash pointer to be passed to the
512 my $conf = $t->{conf};
515 # accepted default key => value pairs to Net::FTP
519 Firewall => undef, # not used
520 FirewallType => undef, # not used
524 Debug => 0, # do not touch
525 Passive => 1, # do not touch
526 Hash => undef, # do not touch
527 LocalAddr => "localhost", # do not touch
531 # This is mostly to fool makeDist
533 exists( $conf->{ClientNameAlias} ) && exists( $conf->{FtpBlockSize} ) &&
534 exists( $conf->{FtpPort} ) && exists( $conf->{FtpTimeout} )
535 or die "Configuration variables for FTP not present in config.pl";
538 # map of options from %Conf in the config.pl scripts to options
539 # the Net::FTP::AutoReconnect object.
542 "Host" => "ClientNameAlias",
543 "BlockSize" => "FtpBlockSize",
545 "Timeout" => "FtpTimeout",
548 foreach my $key ( keys(%$args) ) {
549 $args->{$key} = $conf->{ $argMap->{$key} } || $args->{$key};
553 # Fix for $args->{Host} since it can be in more than one location.
554 # Note the precedence here, this may need to be fixed. Order of
556 # $conf->{ClientNameAlias}
560 $args->{Host} ||= $t->{hostIP};
561 $args->{Host} ||= $t->{host};
564 # return the reference to the hash of items
572 # $dirList = $t->remotels($path);
574 # remotels() returns a reference to a list of hash references that
575 # describe the contents of each file in the directory of the path
578 # In the future, I would like to make this function return objects in
579 # Attrib format. That would be very optimal, and I could probably
580 # release the code to CPAN.
584 my ( $t, $path ) = @_;
588 my $conf = $t->{conf};
590 my ( $dirContents, $remoteDir, $f );
592 unless ( $dirContents = ($path =~ /^\.?$/ ) ? $ftp->dir() :
593 $ftp->dir("$path/") )
596 return "can't retrieve remote directory contents of $path";
599 foreach my $info ( @{parse_dir($dirContents)} ) {
610 # convert & store utf8 version of filename
612 $f->{utf8name} = $f->{name};
613 from_to( $f->{utf8name}, $conf->{ClientCharset}, "utf8" );
616 # construct the full name
618 $f->{fullName} = "$t->{sharePath}/$path/$f->{name}";
619 $f->{fullName} =~ s/\/+/\//g;
621 $f->{relPath} = ($path eq "") ? $f->{name} : "$path/$f->{name}";
622 $f->{relPath} =~ s/\/+/\//g;
624 push( @$remoteDir, $f );
632 # ignoreFileCheck() looks at the attributes of the arguments and the
633 # backup types, and determines if the file should be skipped in this
638 my ( $t, $f, $attrib ) = @_;
641 # case for ignoring the files '.' & '..'
643 if ( $f->{name} =~ /^\.\.?$/ ) {
648 # Check the include/exclude lists. the function returns true if
649 # the file should be backed up, so return the opposite.
651 return ( !$t->checkIncludeExclude( $f->{fullName} ) );
656 # handleSymlink() backs up a symlink.
660 my ( $t, $f, $OutDir, $attrib ) = @_;
662 my $conf = $t->{conf};
664 my ( $target, $targetDesc );
667 type => BPC_FTYPE_SYMLINK,
669 uid => undef, # unsupported
670 gid => undef, # unsupported
672 mtime => $f->{mtime},
676 # If we are following symlinks, back them up as the type of file
677 # they point to. Otherwise, backup the symlink.
679 if ( $conf->{FtpFollowSymlinks} ) {
682 # handle nested symlinks by recurring on the target until a
683 # file or directory is found.
685 $f->{type} =~ /^l (.*)/;
688 if ( $targetDesc = $ftp->dir("$target/") ) {
689 $t->handleSymDir( $f, $OutDir, $attrib, $targetDesc );
691 } elsif ( $targetDesc = $ftp->dir($target) ) {
692 if ( $targetDesc->[4] eq 'file' ) {
693 $t->handleSymFile( $f, $OutDir, $attrib );
695 } elsif ( $targetDesc->[4] =~ /l (.*)/) {
697 $t->logFileAction("fail", $f->{utf8name}, $attribInfo);
709 # If we are not following symlinks, record them normally.
711 $attrib->set( $f->{utf8name}, $attribInfo );
712 $t->logFileAction("create", $f->{utf8name}, $attribInfo);
720 my ($t, $fSym, $OutDir, $attrib, $targetDesc) = @_;
728 my ( $t, $fSym, $OutDir, $attrib, $targetDesc ) = @_;
731 my $conf = $t->{conf};
734 name => $fSym->{name},
735 type => $targetDesc->[1],
736 size => $targetDesc->[2],
737 mtime => $targetDesc->[3],
738 mode => $targetDesc->[4]
741 $f->{utf8name} = $fSym->{name};
742 from_to( $f->{utf8name}, $conf->{ClientCharset}, "utf8" );
744 $f->{relPath} = $fSym->{relPath};
746 $f->{fullName} = "$t->{shareName}/$fSym->{relPath}/$fSym->{name}";
747 $f->{fullName} =~ s/\/+/\//g;
750 # since FTP servers follow symlinks, we can jsut do this:
752 return $t->handleFile( $f, $OutDir, $attrib );
757 # handleDir() backs up a directory, and initiates a backup of its
762 my ( $t, $dir, $OutDir ) = @_;
766 my $conf = $t->{conf};
767 my $view = $t->{view};
768 my $stats = $t->{stats};
770 my ( $exists, $digest, $outSize, $errs );
771 my ( $poolWrite, $poolFile, $attribInfo );
772 my ( $localDir, $remoteDir, $attrib, %expectedFiles );
774 if ( exists($dir->{utf8name})) {
775 $OutDir .= "/" . $bpc->fileNameMangle( $dir->{utf8name} );
778 unless ( -d $OutDir ) {
780 mkpath( $OutDir, 0, 0755 );
781 $t->logFileAction( "create", $dir->{utf8name}, $dir );
784 $attrib = BackupPC::Attrib->new( { compress => $t->{Compress} } );
785 $remoteDir = $t->remotels( $dir->{relPath} );
787 if ( $t->{type} eq "incr" ) {
788 $localDir = $view->dirAttrib( $t->{incrBaseBkupNum},
789 $t->{shareName}, $dir->{relPath} );
790 %expectedFiles = map { $_ => 0 } sort keys %$localDir
794 # take care of each file in the directory
796 SCAN: foreach my $f ( @{$remoteDir} ) {
798 next SCAN if $t->ignoreFileCheck( $f, $attrib );
801 # handle based on filetype
803 if ( $f->{type} eq 'f' ) {
804 $t->handleFile( $f, $OutDir, $attrib );
806 } elsif ( $f->{type} eq 'd' ) {
809 type => BPC_FTYPE_DIR,
811 uid => undef, # unsupported
812 gid => undef, # unsupported
814 mtime => $f->{mtime},
817 #print STDERR "$f->{utf8name}: ". Dumper($attribInfo);
819 if ( $t->handleDir($f, $OutDir) ) {
820 $attrib->set( $f->{utf8name}, $attribInfo);
823 } elsif ( $f->{type} =~ /^l (.*)/ ) {
824 $t->handleSymlink( $f, $OutDir, $attrib );
833 # Mark file as seen in expected files hash
835 $expectedFiles{ $f->{utf8name} }++ if ( $t->{type} eq "incr" );
837 } # end foreach (@{$remoteDir})
840 # If the backup type is incremental, mark the files that are not
841 # present on the server as deleted.
843 if ( $t->{type} eq "incr" ) {
844 while ( my ($f, $seen) = each %expectedFiles ) {
845 $attrib->set( $f, { type => BPC_FTYPE_DELETED } )
851 # print the directory attributes, now that the directory is done.
853 my $fileName = $attrib->fileName($OutDir);
854 my $data = $attrib->writeData();
856 $poolWrite = BackupPC::PoolWrite->new( $bpc, $fileName, length($data),
858 $poolWrite->write( \$data );
859 ( $exists, $digest, $outSize, $errs ) = $poolWrite->close();
869 # handleFile() backs up a file.
873 my ( $t, $f, $OutDir, $attrib ) = @_;
877 my $view = $t->{view};
878 my $stats = $t->{stats};
879 my $newFilesFH = $t->{newFilesFH};
881 my ( $poolFile, $poolWrite, $data, $localSize );
882 my ( $exists, $digest, $outSize, $errs );
887 # If this is an incremental backup and the file exists in a
888 # previous backup unchanged, write the attribInfo for the file
891 if ( $t->{type} eq "incr" ) {
892 return 1 if $t->incrFileExistCheck( $f, $attrib );
896 type => BPC_FTYPE_FILE,
898 uid => undef, # unsupported
899 gid => undef, # unsupported
901 mtime => $f->{mtime},
905 # If this is a full backup or the file has changed on the host,
908 unless ( tie( *FTP, 'Net::FTP::RetrHandle', $ftp, $f->{fullName} ) ) {
910 $t->handleFileAction( "fail", $attribInfo );
911 $t->{xferBadFileCnt}++;
916 $poolFile = $OutDir . "/" . $bpc->fileNameMangle( $f->{name} );
917 $poolWrite = BackupPC::PoolWrite->new( $bpc, $poolFile, $f->{size},
918 $bpc->{xfer}{compress} );
923 $localSize += length($_);
924 $poolWrite->write( \$_ );
926 ( $exists, $digest, $outSize, $errs ) = $poolWrite->close();
929 # calculate the file statistics
933 $t->logFileAction( "fail", $f->{utf8name}, $attribInfo );
935 $t->{xferBadFileCnt}++;
936 $t->{errCnt} += scalar(@$errs);
941 # this should never happen
943 if ( $localSize != $f->{size} ) {
945 $t->logFileAction( "fail", $f->{utf8name}, $attribInfo );
947 $stats->{xferBadFileCnt}++;
955 $attrib->set( $f->{utf8name}, $attribInfo );
956 $t->logFileAction( $exists ? "pool" : "create", $f->{utf8name}, $attribInfo );
957 print $newFilesFH "$digest $f->{size} $poolFile\n" unless $exists;
962 $stats->{TotalFileCnt}++;
963 $stats->{ExistFileCnt}++;
964 $stats->{ExistFileCompSize} += -s $poolFile;
965 $stats->{ExistFileSize} += $f->{size};
966 $stats->{TotalFileSize} += $f->{size};
968 $t->{byteCnt} += $localSize;
974 # this function checks if the file has been modified on disk, and if
975 # it has, returns. Otherwise, it updates the attrib values.
977 sub incrFileExistCheck
979 my ($t, $f, $attrib) = @_;
981 my $view = $t->{view};
983 my $oldAttribInfo = $view->fileAttrib( $t->{incrBaseBkupNum},
984 $t->{shareName}, $f->{relPath} );
986 #print STDERR "*" x 50 . "\n";
987 #print STDERR "Old data:\n" . Dumper($oldAttribInfo);
988 #print STDERR "New data:\n" . Dumper($f);
989 #print STDERR "$f->{fullName}: $oldAttribInfo->{mtime} ?= $f->{mtime}, $oldAttribInfo->{size} ?= $f->{size}\n";
991 return ( $oldAttribInfo->{mtime} == $f->{mtime}
992 && $oldAttribInfo->{size} == $f->{size} );
997 # Generate a log file message for a completed file. Taken from
998 # BackupPC_tarExtract. $f should be an attrib object.
1002 my ( $t, $action, $name, $attrib ) = @_;
1004 my $owner = "$attrib->{uid}/$attrib->{gid}";
1006 ( ( "", "p", "c", "", "d", "", "b", "", "", "", "l", "", "s" ) )
1007 [ ( $attrib->{mode} & S_IFMT ) >> 12 ];
1009 $name = "." if ( $name eq "" );
1010 $owner = "-/-" if ( $owner eq "/" );
1012 my $fileAction = sprintf( " %-6s %1s%4o %9s %11.0f %s\n",
1013 $action, $type, $attrib->{mode} & 07777,
1014 $owner, $attrib->{size}, $name );
1016 return $t->logWrite( $fileAction, 1 );