#!/usr/bin/perl ######################################################################## ### ### ### script : downsync ### ### ### ### date : 06.09.2001, hoja ### ### 10.12.2006, hoja ### ### 31.01.2008, hoja ### ### ### ### purpose: synchronise local directory with FTP server directory ### ### ### ######################################################################## $DIRtarget = 'e:\tmp dir'; ### target directory name (abs. path!) #$FTPserver = 'localhost'; ### keep for localhost-login #$FTPuser = 'xxxx'; ### user on ftp server #$FTPpass = 'xxxx'; ### password to login on FTP server #$FTPpath = 'Dokumente'; ### source directory on FTP server ######################################################################## ### ### ### END OF CONFIGURATION ### ### ### ######################################################################## use Cwd; use Net::FTP; use File::Spec; use POSIX qw/strftime/; $DIRtarget = $ENV{'DOWNSYNC_TARGET'} if $ENV{'DOWNSYNC_TARGET'}; $FTPserver = $ENV{'DOWNSYNC_SERVER'} if $ENV{'DOWNSYNC_SERVER'}; $FTPuser = $ENV{'DOWNSYNC_USER'} if $ENV{'DOWNSYNC_USER'}; $FTPpass = $ENV{'DOWNSYNC_PASS'} if $ENV{'DOWNSYNC_PASS'}; $FTPpath = $ENV{'DOWNSYNC_PATH'} if $ENV{'DOWNSYNC_PATH'}; do {print <<'USAGE'; exit 2} if $ARGV[0] =~ /[-]+h(elp)?/; Please set the following environment parameters (or edit the script itself) before running "downsync": $DOWNSYNC_TARGET Local target sirectory $DOWNSYNC_SERVER FFT server name or IP address $DOWNSYNC_USER User name on FTP-server. $DOWNSYNC_PASS Password $DOWNSYNC_PATH Source directory on FTP server for "downsync" (must exist) How this works: This program compares the time stamps of all files in the source directory tree on the FTP server with the files in the local target directory tree. downsync then transfers all files and directories from the FTP server that do not yet exist locally, or are not of the exact same age than the local files. The script returns '0' if one or more files had to be transferred or deleted, or '1' if the directory trees have been found to be identical. Any other higher exit code indicates an error. Optional parameters: --help = This text. --delta-print = Print the names of a files that differ on STDOUT, while all regular status messages go to STDERR to allow for piping the file names into other programs.. USAGE ### keep dir separator portable $DIRtarget = File::Spec->canonpath($DIRtarget); $channel = ($ARGV[0] =~ /--delta-print/) ? 'STDERR' : 'STDOUT'; print $channel "\nLoading local file list \"$DIRtarget\"....\n\n"; mkdir($DIRtarget, 0777); getdirlist($DIRtarget); print $channel "Connecting to FTP server \"$FTPserver\"\n\n"; ### connect to ftp server my $ftp = Net::FTP->new($FTPserver) or die "Connection failed! ($!)\n"; ### einloggen $ftp->login($FTPuser, $FTPpass) or die "FTP server login failed! ($!)\n"; print $channel "Loading file list from FTP server...\n\n"; getftplist($FTPpath); print $channel "Determining file delta...\n\n"; foreach $ftpfile (keys %FTPLIST) { $localfile = File::Spec->canonpath($ftpfile); ### files on the server, but not on local disk? if(not exists $DIRLIST{$localfile}) { push @download, $ftpfile; } ### what files (not directories) have been modified? elsif($FTPLIST{$ftpfile} !~ /^d/) { my ($lstamp) = ($DIRLIST{$localfile} =~ /(\d)+/); my ($fstamp) = ($FTPLIST{$ftpfile} =~ /(\d)+/); if($lstamp == $fstamp or $lstamp+1 == $fstamp or $lstamp-1 == $fstamp) { push @download, $ftpfile; } } } print $channel "Staring file transfer...\n"; $ftp->cwd($FTPpath) or die "Cannot chdir into $FTPpath on $FTPserver\n"; $download = $deleted = 0; foreach $delta ( sort { split(/[\/\\]/,$a) <=> split(/[\/\\]/,$b) } @download) { ### build full ftp file path (eliminate duplicate slashes) $ftpfile = "$FTPpath/$delta"; $ftpfile =~ tr/\//\//s; print $channel "Downloading: $ftpfile\n"; print "$ftpfile\n" if $ARGV[0] =~ /--delta-print/; $target = File::Spec->join($DIRtarget,$delta); $FTPLIST{$delta} =~ /^d/ ? mkdir($target, 0777) : $ftp->get("$ftpfile",$target); ### set time stamp on local file if($FTPLIST{$delta} !~ /^d/) { ($stamp) = ($FTPLIST{$delta} =~ /(\d)+/); utime $stamp,$stamp,(File::Spec->join($DIRtarget,$delta)); $download++; } } ### terminate FTP connection $ftp->quit; chdir($oldcwd); ### check for files that are no longer on the FTP server, but exist locally foreach $localfile (keys %DIRLIST) { ($ftpfile = $localfile) =~ s/^\Q$DIRtarget\E[\/\\]//i; $ftpfile =~ s#\\#/#g; ### vice versa: fiel on ftp server, but not locally? if(not exists $FTPLIST{$ftpfile}) { push @localonly, $localfile; } } if(@localonly) { print $channel "\nThese files/directories do not exist on the FTP server any more:\n"; ### delete files first, then directories foreach $localonly ( sort {split(/[\/\\]/,$b)<=>split(/[\/\\]/,$a)} @localonly) { $target = File::Spec->join($DIRtarget,$localonly); print $channel "Deleting local: $target\n"; if($DIRLIST{$localonly} =~ /^d/) { chdir(".."); rmdir($target) or warn "Directory $target connot be removed! ($!)"; ###print("D: ".File::Spec->join($DIRtarget,$localonly)); } else { ### delete local file unlink($target) or warn "File $target cannot be removed! ($!)"; ###print("F: ".File::Spec->join($DIRtarget,$localonly)); } } print $channel "\n"; $deleted++; } print $channel "\nSynchronisation complete! " . "($download files have been transferred, $deleted removed)\n\n"; ($download or $deleted) ? exit(0) : exit(1); ################ sub getdirlist { ################ my $dir = shift; my ($entry, $fullname, $relname, @entries); chdir($dir) or die "CWD to $dir failed! ($!)\n"; ### paths are relative to the base dir opendir DIR, $dir; ### read local dir foreach $entry (grep { !/^\.$|^\.\.$/ } readdir(DIR)) { my $fullname = File::Spec->join($dir, $entry); ($relname = $fullname) =~ s/^\Q$DIRtarget\E[\/\\]//i; ### file or dir? $type = (-d $fullname) ? 'd' : 'f'; ### "[df] " $DIRLIST{"$relname"}="$type " . (stat("$fullname"))[9]; ###print $channel "$relname - $fullname: $DIRLIST{$relname}\n"; ### nter recursion &getdirlist($fullname) if -d $fullname; } closedir DIR; } ################ sub getftplist { ################ ### get parameter from stack my $dir = shift; my ($entry, $fullname, $filename, $type, @dir); $ftp->cwd($dir) or return; ### get dir content in long format holen (ls -l) @dir = $ftp->dir; ### paths are relative to the base dir ### read ftp server dir (short format) foreach $entry ($ftp->ls) { next if $entry eq '.' or $entry eq '..'; $fullname = ($dir =~ /\/$/) ? $dir.$entry : $dir."/".$entry; $filename = $fullname; $filename =~ s/^$FTPpath\///; ### dir or file? $pattern = "[" . (join '][', split //, $entry) . "]"; $type = (join ' ', grep {/$pattern/} @dir) =~ /^d/ ? 'd':'f'; ### "[df] " $FTPLIST{$filename}="$type " . $ftp->mdtm("$fullname"); ###print $channel "$filename: $FTPLIST{$filename}\n"; ### enter recursion &getftplist($fullname) if $type eq 'd'; } }