#!/usr/bin/ruby
=begin
FTP Sync allows synchronizing files on multiple client machines via an
FTP server.
U wrote this because my web host does not run CVS. Additionally,
I usually sync large binary files, and I need only the latest version.
FTP Sync saves file information (version and timestamp) in an XML file
which is stored on the FTP server alognside the file with content.
The program compares the version number in the remote XML file, and
the time stamp on the local XML file, figuring out whether to upload
the local version or download the remote one.
Usage:
ftp_sync.rb "C:\dir\subdir\file.ext"
Configuration:
Edit the following files:
config.ini - specifies server info, and local root dir
control.xml - keeps version/timestamp for all files
CONFIG.INI
A typical 'config.ini' file looks like this:
----------------------------------------
host = ftp.server.com
username = john
password = john_is_genius
remote_root = my_proj/
remote_control = my_proj/control.xml
local_root = C:\John\projects
control_filename = C:\John\projects\control.xml
temp_filename = C:\John\projects\control.tmp.xml
----------------------------------------
It is read into a hash:
c = {
'host' => "ftp.server.com",
'username' => "john",
'password' => "john_is_genius",
...
}
The hash can be accessed either as c['host'] or c.host
CONTROL.XML
my_file.bin
subdir/file.bin
"version" is incremented every time the new version of the file is
uploaded.
"time" is time on the local machine, and it is compared
against the file modification time. Once on the remote server, "time"
has no meaning, because the server and other clients do NOT need to
have synchronized clocks and timezones.
Using xml-simple library, this file is read into a Hash-like object
which can be accessed as:
i = doc.file.find{|x| x.content == 'my_file.bin' }
i.time == "Sat May 01 21:20:16 2004"
i.version == "16"
i.content == "my_file.bin"
=end
# require_local() loads a ruby file (just like 'require' does)
# but the argument is relative to the current script. This
# is default in Java import and C/C++ include, but in Ruby
# this function is needed.
def require_local file
# WARNING: in the required file, __FILE__ == "./xyz.rb"
wdir= Dir.getwd
Dir.chdir File.dirname(__FILE__)
require File.join('.',file)
Dir.chdir wdir
end
require 'net/ftp'
require 'rexml/document'
require 'xmlsimple'
require 'ParseDate'
require_local 'utils.rb'
include ParseDate
#
# This script looks up config file located in the same
# directory as this script. Initially, I tried finding out
# the script deroctory by extracting the path from __FILE__.
# __FILE__ == "./ftp_sync.rb", but after the working directory
# changes, the path points to the current directory instead of the
# directory with this script. This constant evaluates when the
# script is loaded/required, and the full path is expanded and saved.
#
PROGRAM_DIR = File.expand_path(File.dirname(__FILE__))
SYNC_CONFLICT_ERROR_MESSAGE = < curr_ver
# The remote file is more recent.
if modified_local_file
# WARNING: modified BOTH local and remote file !!!
message SYNC_CONFLICT_ERROR_MESSAGE
pause
else
message "Remote file newer. Downloading newer version."
ftp.getbinaryfile c.remote_filename, c.local_filename {
print "#"; $stdout.flush
}
print "\n"
ftp.getbinaryfile c.remote_control, c.control_filename
update_local_time c.control_filename, c.filename, c.local_filename
message "File downloaded successfully."
end
end
end
def relative_path filename, path, raise_exception=true
filename = filename.gsub('\\','/')
path = path.gsub('\\','/')
if not path.ends_with? '/'
path += '/'
end
if filename.begins_with? path
return filename[ path.length .. -1 ]
else
if raise_exception
raise "File not in the subpath"
else
return nil
end
end
end
#
# Reads from the control file
# the local time when the content file was last modified.
#
def get_timestamp( control_file, content_file )
doc = XmlSimple.xml_in control_file
i = doc.file.find{|x| x.content == content_file }
return Time.local( * parsedate(i.time))
end
def get_last_version( control_file, content_file )
doc = XmlSimple.xml_in control_file
i = doc.file.find{|x| x.content == content_file }
return i.version.to_i
end
#
# control_file == "control.xml"
# filename == "my_file.bin"
# content_file == "C:\temp\myfile.bin"
#
def increment_version( control_file, filename, content_file )
doc = XmlSimple.xml_in control_file
i = doc.file.find{|x| x.content == filename }
raise "Entry for file '#{filename}' not found in '#{control_file}'" if i.nil?
version = i.version.to_i
version += 1
i.version = version.to_s
i.time = file_time_string(content_file)
File.open( control_file, "w" ){|f|
f << XmlSimple.xml_out(doc)
}
end
def update_local_time( control_file, filename, content_file )
debug "Updating local time"
doc = XmlSimple.xml_in control_file
i = doc.file.find{|x| x.content == filename }
raise "Entry for file '#{filename}' not found in '#{control_file}'" if i.nil?
# Update local time in the control file, because the clock on the
# local machine is different from the remote clocks.
i.time = file_time_string(content_file)
File.open( control_file, "w" ){|f|
f << XmlSimple.xml_out(doc)
}
end
def file_time_string filename
File.new(filename).mtime.asctime
end
if __FILE__ == $0
ftp_sync ARGV[0]
end