Skip to content

Instantly share code, notes, and snippets.

@mattmccray
Created July 21, 2008 22:06
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save mattmccray/352 to your computer and use it in GitHub Desktop.
Save mattmccray/352 to your computer and use it in GitHub Desktop.
require 'find'
require 'rubygems'
require 'digest/md5'
gem 'net-sftp', '<2.0.0'
require 'net/sftp'
class SftpDirPublisher
CHECKSUM_FILENAME = ".checksums"
attr_reader :host, :username, :password, :remote_dir, :local_dir, :exclude
def initialize(host, username, password, remote_dir, local_dir, exclude=[])
@host = host
@username = username
@password = password
@remote_dir = remote_dir
@local_dir = local_dir
@verbose = true
@exclude = exclude
end
def upload(dry_run=false)
create_checksums
puts_if_verbose "Connecting to #{@host}..."
Net::SFTP.start(@host, @username, @password, :timeout=>30) do |sftp|
puts_if_verbose "Fetching checksums..."
local_checksums = YAML::load(File.open( local_path(CHECKSUM_FILENAME) ))
remote_checksums = {}
begin
checksums_src = ''
sftp.open_handle( server_path(CHECKSUM_FILENAME) ) do |handle|
checksums_src = sftp.read(handle)
end
remote_checksums = YAML::load(checksums_src) unless checksums_src.nil? or checksums_src==''
rescue Net::SFTP::Operations::StatusException=>se
# server's checksum.yml is missing
end
puts_if_verbose "Comparing checksum data..."
to_upload, to_remove = hash_diff(local_checksums, remote_checksums)
if to_upload.length > 0 or to_remove.length > 0
puts_if_verbose "Differences found:"
to_upload.each {|f| puts_if_verbose " - (Upload) #{f}" }
to_remove.each {|f| puts_if_verbose " - (Delete) #{f}"}
exit if dry_run
puts_if_verbose "Beginning sync..."
to_upload.each do |filename|
begin
puts_if_verbose " - #{remote_path(filename)}"
dir_name = File.dirname(filename)
dir_segs = dir_name.split('/')
#puts "Checking path: #{dir_segs.join( '/' )}"
prog_path = []
dir_segs.each do |partial_dir|
begin
prog_path << partial_dir
sftp.mkdir( remote_path( prog_path ), :permissions=>0755 )
puts_if_verbose " + #{remote_path( prog_path )}"
rescue Net::SFTP::Operations::StatusException=>se
# don't worry about it
end
end
sftp.put_file local_path(filename), remote_path(filename)
sftp.open_handle( remote_path(filename) ) do |handle|
sftp.fsetstat( handle, :permissions=>0644 )
end
rescue Net::SFTP::Operations::StatusException=>se
puts_if_verbose " ! Error uploading '#{filename}': #{se}"
puts_if_verbose; puts_if_verbose "Halted execution of upload."
exit(1)
end
end
to_remove.each do |filename|
begin
sftp.remove remote_path(filename)
puts_if_verbose " x #{remote_path(filename)}"
rescue
puts_if_verbose " ! Error removing '#{filename}': #{$!}"
end
end
begin
sftp.put_file local_path(CHECKSUM_FILENAME), remote_path(CHECKSUM_FILENAME)
sftp.open_handle( remote_path(CHECKSUM_FILENAME) ) do |handle|
sftp.fsetstat( handle, :permissions=>0644 )
end
rescue
puts_if_verbose " ! Error uploading '#{CHECKSUM_FILENAME}': #{$!}"
end
summary = "#{to_upload.length} file(s) uploaded"
summary += ", #{to_remove.length} files(s) deleted" if to_remove.length > 0
puts summary
else
puts "No changes made. The server is up to date!"
end
puts "Done."
end
end
protected
def create_checksums
checksums = generate_from(@local_dir)
File.open(local_path(CHECKSUM_FILENAME), 'w') do |f|
f.write checksums.to_yaml
end
checksums
end
# returns [files_to_upload, files_to_delete]
def hash_diff(source={}, target={})
# look for differences...
src_files = source.fetch('files', {})
tgt_files = target.fetch('files', {})
to_update = []; to_delete = []; to_upload = []
tgt_files.each do |filename, checksum|
if src_files.has_key? filename
to_update << filename unless src_files[filename] == checksum
else
to_delete << filename
end
end
to_upload = src_files.keys - tgt_files.keys
# returns [files_to_upload, files_to_delete]
[[to_upload, to_update].flatten.sort, to_delete.sort]
end
# Create the checksums...
def generate_from(cache_dir)
checksums = { 'generated_on'=>Time.now, 'files'=>{} }
checksum_path = local_path(CHECKSUM_FILENAME)
puts "Generating checksums from #{cache_dir}"
Find.find( cache_dir ) do |f|
next if File.directory?( f ) or f == checksum_path or has_exclusion(f,cache_dir)
checksums['files'][f.gsub("#{cache_dir}/", '')] = ::Digest::MD5.hexdigest( File.read(f) )
end
checksums
end
def has_exclusion(path, cache_dir)
path = path.gsub("#{cache_dir}/", '')
exclude.any? do |test_pattern|
case test_pattern
when String: path.start_with?(test_pattern)
when Regexp: path =~ test_pattern
end
end
end
def server_path(path)
[@remote_dir, path].flatten.join('/')
end
alias_method :remote_path, :server_path
def local_path(path)
File.join(@local_dir, path)
end
def puts_if_verbose(msg='')
puts(msg) if @verbose
end
end
class String
def starts_with?(prefix)
prefix = prefix.to_s
self[0, prefix.length] == prefix
end
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment