#!/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