#!/usr/bin/perl ######################################################################## ### ### ### script : upsync ### ### ### ### datu : 22.08.2001, 24.10.2004, 10.12.2006, 30.01.08, hoja ### ### ### ### purpose: synchronise local directory with FTP server directory ### ### ### ######################################################################## ### source directory name: my $DIRsource = 'e:\tmp dir'; #$FTPserver = 'localhost'; ### keep for localhost-login #$FTPuser = 'xxxx'; ### user on ftp server #$FTPpass = 'yyyy'; ### password to login on ftp server #$FTPpath = '/public_html'; ### target directory (must exist!) ######################################################################## ### ### ### END OF CONFIGURATION ### ### ### ######################################################################## use Net::FTP; use DB_File; use File::Spec; use File::Basename; use POSIX qw/strftime/; my $DIRsource = $ENV{'UPSYNC_SOURCE'} if $ENV{'UPSYNC_SOURCE'}; my $FTPserver = $ENV{'UPSYNC_SERVER'} if $ENV{'UPSYNC_SERVER'}; my $FTPuser = $ENV{'UPSYNC_USER'} if $ENV{'UPSYNC_USER'}; my $FTPpass = $ENV{'UPSYNC_PASS'} if $ENV{'UPSYNC_PASS'}; my $FTPpath = $ENV{'UPSYNC_PATH'} if $ENV{'UPSYNC_PATH'}; do {print <<'USAGE'; exit 1} if $ARGV[0] =~ /[-]+h(elp)?/; Please set the following environment parameters (or edit the script itself) before running "upsync": $UPSYNC_SOURCE Local source sirectory $UPSYNC_SERVER FFT server name or IP address $UPSYNC_USER User name on FTP-server. $UPSYNC_PASS Password $UPSYNC_PATH Target directory on FTP server for "upsync" (must exist) How this works: This program compares the time stamps of all files in the local directory tree with the files in the target directory tree on the FTP server. Upsync then transfers all files and directories to the FTP server that do not yet exist on the server, or are not of the exact same age than the local files. After the synchronisation has been completed, upsync determines the time stamps of the files just transferred to the FTP server, and applies it to the local source file. This way, both sides carry the exact same time stamp. All files in the target directory on the FTP server that do not exist locally any more are being deleted (ditto for directries). Optional parameters: --help = This text. --local-build-db = If you are sure that the files on the FTP server ARE identical to the local directory, then you may use this flag to re-build the time stamp database file. This will be necessary if the time stamps on one side have been changed, but not the file content. Anyway, this is faster than re-syncing the entire source tree. USAGE ### keep dir separator portable $DIRsource = File::Spec->canonpath($DIRsource); $local_build_db=1 if $ARGV[0] =~ /--local-build-db/; print "\nLoading local file list from \"$DIRsource\"....\n\n"; getdirlist($DIRsource); chdir($oldcwd); if($local_build_db) { print "Creating transfer database file...\n\n"; $db_comp = File::Spec->join($DIRsource, "upsync.db"); tie(%UPLOADED, 'DB_File', "$db_comp", O_WRITE|O_CREAT, 0644) or die "Database tie against $db_comp failed! ($!)"; %UPLOADED = map { my $key=$_; ( my $new_key = $_ ) =~ s/^\Q$DIRsource\E[\/\\]//; $new_key => $DIRLIST{$key} } keys %DIRLIST; untie %UPLOADED; print "Done.\n"; exit; } print "Loading list of most recent FTP server uploads....\n\n"; $db_comp = File::Spec->join($DIRsource, "upsync.db"); tie(%UPLOADED, 'DB_File', "$db_comp", O_RDWR|O_CREAT, 0644) or die "Database tie against $db_comp failed!"; print "Determining file delta....\n\n"; foreach $localfile (keys %DIRLIST) { ($ftpfile = $localfile) =~ s/^\Q$DIRsource\E[\/\\]//; $ftpfile =~ s#\\#/#g; ### files on the hard drive, but not on the server? if(not exists $UPLOADED{$ftpfile}) { push @upload, $ftpfile; } ### what files (not directories) have been modified? elsif($DIRLIST{$localfile} !~ /^d/) { my ($lstamp) = ($DIRLIST{$localfile} =~ /(\d+)/); my ($fstamp) = ($UPLOADED{$ftpfile} =~ /(\d+)/); if($lstamp != $fstamp and $lstamp+1 != $fstamp and $lstamp-1 != $fstamp) { push @upload, $ftpfile; } } } ### check which local files have been deleted, but are still on the server foreach $ftpfile (keys %UPLOADED) { $localfile = File::Spec->join($DIRsource,$ftpfile); ### und nun umgekehrt: welche files gibt's ftpseitig, aber nicht lokal? if(not exists $DIRLIST{$localfile}) { push @ftponly, $ftpfile; } } if(not @upload and not @ftponly) { print "Nothing to do.\n\n"; exit; } print "Connecting to FTP server \"$FTPserver\"....\n\n"; my $ftp = Net::FTP->new($FTPserver, Passive => 1) or die "Connecting to FTP server failed! ($!)\n"; $ftp->login($FTPuser, $FTPpass) or die "Login failed! ($!)\n"; $ftp->binary(); $FTPbase = $ftp->pwd; $ftp->mkdir($FTPpath); print "Starting file transfer...\n"; $ftp->cwd("$FTPbase/$FTPpath") or die "Can not chdir into $FTPbase/$FTPpath on $FTPserver!\n"; $anzahl = 0; foreach $delta ( sort { split(/[\/\\]/,$a) <=> split(/[\/\\]/,$b) } @upload) { $localfile = File::Spec->join($DIRsource,$delta); print "Uploading: $localfile\n"; $DIRLIST{$localfile} =~ /^d/ ? $ftp->mkdir("$FTPbase/$FTPpath/$delta") : $ftp->put($localfile, "$FTPbase/$FTPpath/$delta") or do { print "uploading $localfile failed! ($!)!\n"; $ftp->delete("$FTPbase/$FTPpath/$delta"); }; $UPLOADED{$delta} = $DIRLIST{$localfile}; $anzahl++; } if(@ftponly) { print "\nThese files/directories exist only on the FTP server:\n"; ### ftpueberschussliste von unten nach oben durchlaufen (jeweils erst ### die dateien loeschen, dann die dazugehoerenden verzeichnisse).... ### foreach $ftponly ( sort { split(/[\/\\]/,$b) <=> split(/[\/\\]/,$a) } @ftponly) { print "Deleting from server: $ftponly\n"; if($UPLOADED{$ftponly} =~ /^d/) { $ftp->rmdir("$FTPbase/$FTPpath/$ftponly"); } else { $ftp->delete("$FTPbase/$FTPpath/$ftponly"); } delete $UPLOADED{$ftponly}; } print "\n"; } ### terminate connection $ftp->quit; print "\nSynchronisation complete. " . "($anzahl files have been transferred)\n\n"; exit; ################ sub getdirlist { ################ my $dir = shift; my ($entry, $fullname, $relname, @entries); chdir($dir) or die "CWD into $dir failed! ($!)\n"; ### paths are relative to the base directory opendir DIR, $dir; ### read local directory foreach $entry (grep { !/^\.$|^\.\.$/ } readdir(DIR)) { next if $entry =~ m#upsync.db#; my $fullname = File::Spec->join($dir, $entry); ($relname = $fullname) =~ s/^\Q$DIRsource\E[\/\\]//i; ### file or directory? $type = (-d $fullname) ? 'd' : 'f'; ### "[df] " $DIRLIST{$fullname} = "$type " . (stat("$fullname"))[9]; ###print "$relname: $DIRLIST{$fullname}\n"; ### enter recursion &getdirlist($fullname) if -d $fullname; } closedir DIR; } ################ sub getftplist { ################ ### get parameter from stack my $dir = shift; my ($entry, $fullname, $filename, $type); $ftp->cwd($dir) or return; ### paths are relative to the base directory ### get directory content in long format (ls -l) foreach $dirline ($ftp->dir) { $entry = (split / /, $dirline)[-1]; ### don't process '.' and '..' next if $entry eq '.' or $entry eq '..'; $fullname = ($dir eq '/') ? $dir.$entry : $dir."/".$entry; $filename = $fullname; $filename =~ s/^$FTPbase\/$FTPpath\///; ### file or directory? $pattern = "[" . (join '][', split //, $entry) . "]"; $type = $dirline =~ /^d/ ? 'd':'f'; ### "[df] " $FTPLIST{$filename}="$type " . $ftp->mdtm("$fullname"); ###debug### print "$filename: $FTPLIST{$filename}\n"; ### enter recursion &getftplist($fullname) if $type eq 'd'; } }