Created
July 21, 2008 22:06
-
-
Save mattmccray/352 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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