diff --git a/plugins/communicators/winrm/file_manager.rb b/plugins/communicators/winrm/file_manager.rb new file mode 100644 index 000000000..87a87438c --- /dev/null +++ b/plugins/communicators/winrm/file_manager.rb @@ -0,0 +1,166 @@ +require "log4r" + +module VagrantPlugins + module CommunicatorWinRM + + # Manages the file system on the remote guest allowing for file tranfer + # between the guest and host. + class FileManager + + def initialize(shell) + @logger = Log4r::Logger.new("vagrant::communication::filemanager") + @shell = shell + end + + # Uploads the given file or directory from the host to the guest (recursively). + # + # @param [String] The source file or directory path on the host + # @param [String] The destination file or directory path on the host + def upload(host_src_file_path, guest_dest_file_path) + @logger.debug("Upload: #{host_src_file_path} -> #{guest_dest_file_path}") + if File.directory?(host_src_file_path) + upload_directory(host_src_file_path, guest_dest_file_path) + else + upload_file(host_src_file_path, guest_dest_file_path) + end + end + + # Downloads the given file from the guest to the host. + # NOTE: This currently only supports single file download + # + # @param [String] The source file path on the guest + # @param [String] The destination file path on the host + def download(guest_src_file_path, host_dest_file_path) + @logger.debug("#{guest_src_file_path} -> #{host_dest_file_path}") + + output = @shell.powershell("[System.convert]::ToBase64String([System.IO.File]::ReadAllBytes(\"#{guest_src_file_path}\"))") + contents = output[:data].map!{|line| line[:stdout]}.join.gsub("\\n\\r", '') + out = Base64.decode64(contents) + IO.binwrite(host_dest_file_path, out) + end + + + private + + # Recursively uploads the given directory from the host to the guest + # + # @param [String] The source file or directory path on the host + # @param [String] The destination file or directory path on the host + def upload_directory(host_src_file_path, guest_dest_file_path) + glob_patt = File.join(host_src_file_path, '**/*') + Dir.glob(glob_patt).select { |f| !File.directory?(f) }.each do |host_file_path| + guest_file_path = guest_file_path(host_src_file_path, guest_dest_file_path, host_file_path) + upload_file(host_file_path, guest_file_path) + end + end + + # Uploads the given file, but only if the target file doesn't exist + # or its MD5 checksum doens't match the host's source checksum. + # + # @param [String] The source file path on the host + # @param [String] The destination file path on the guest + def upload_file(host_src_file_path, guest_dest_file_path) + if should_upload_file?(host_src_file_path, guest_dest_file_path) + tmp_file_path = upload_to_temp_file(host_src_file_path) + decode_temp_file(tmp_file_path, guest_dest_file_path) + else + @logger.debug("Up to date: #{guest_dest_file_path}") + end + end + + # Uploads the given file to a new temp file on the guest + # + # @param [String] The source file path on the host + # @return [String] The temp file path on the guest + def upload_to_temp_file(host_src_file_path) + tmp_file_path = File.join(guest_temp_dir, "winrm-upload-#{rand()}") + @logger.debug("Uploading '#{host_src_file_path}' to temp file '#{tmp_file_path}'") + + base64_host_file = Base64.encode64(IO.binread(host_src_file_path)).gsub("\n",'') + base64_host_file.chars.to_a.each_slice(8000-tmp_file_path.size) do |chunk| + out = @shell.cmd("echo #{chunk.join} >> \"#{tmp_file_path}\"") + raise_upload_error_if_failed(out, host_src_file_path, tmp_file_path) + end + + tmp_file_path + end + + # Moves and decodes the given file temp file on the guest to its + # permanent location + # + # @param [String] The source base64 encoded temp file path on the guest + # @param [String] The destination file path on the guest + def decode_temp_file(guest_tmp_file_path, guest_dest_file_path) + @logger.debug("Decoding temp file '#{guest_tmp_file_path}' to '#{guest_dest_file_path}'") + out = @shell.powershell <<-EOH + $tmp_file_path = [System.IO.Path]::GetFullPath('#{guest_tmp_file_path}') + $dest_file_path = [System.IO.Path]::GetFullPath('#{guest_dest_file_path}') + + if (Test-Path $dest_file_path) { + rm $dest_file_path + } + else { + $dest_dir = ([System.IO.Path]::GetDirectoryName($dest_file_path)) + New-Item -ItemType directory -Force -Path $dest_dir + } + + $base64_string = Get-Content $tmp_file_path + $bytes = [System.Convert]::FromBase64String($base64_string) + [System.IO.File]::WriteAllBytes($dest_file_path, $bytes) + EOH + raise_upload_error_if_failed(out, guest_tmp_file_path, guest_dest_file_path) + end + + # Checks to see if the target file on the guest is missing or out of date. + # + # @param [String] The source file path on the host + # @param [String] The destination file path on the guest + # @return [Boolean] True if the file is missing or out of date + def should_upload_file?(host_src_file_path, guest_dest_file_path) + local_md5 = Digest::MD5.file(host_src_file_path).hexdigest + cmd = <<-EOH + $dest_file_path = [System.IO.Path]::GetFullPath('#{guest_dest_file_path}') + + if (Test-Path $dest_file_path) { + $crypto_provider = new-object -TypeName System.Security.Cryptography.MD5CryptoServiceProvider + try { + $file = [System.IO.File]::Open($dest_file_path, [System.IO.Filemode]::Open, [System.IO.FileAccess]::Read) + $guest_md5 = ([System.BitConverter]::ToString($crypto_provider.ComputeHash($file))).Replace("-","").ToLower() + } + finally { + $file.Dispose() + } + if ($guest_md5 -eq '#{local_md5}') { + exit 0 + } + } + exit 1 + EOH + @shell.powershell(cmd)[:exitcode] == 1 + end + + # Creates a guest file path equivalent from a host file path + # + # @param [String] The base host directory we're going to copy from + # @param [String] The base guest directory we're going to copy to + # @param [String] A full path to a file on the host underneath host_base_dir + # @return [String] The guest file path equivalent + def guest_file_path(host_base_dir, guest_base_dir, host_file_path) + rel_path = File.dirname(host_file_path[host_base_dir.length, host_file_path.length]) + File.join(guest_base_dir, rel_path, File.basename(host_file_path)) + end + + def guest_temp_dir + @guest_temp ||= (@shell.cmd('echo %TEMP%'))[:data][0][:stdout].chomp + end + + def raise_upload_error_if_failed(out, from, to) + raise Errors::WinRMFileTransferError, + :from => from, + :to => to, + :message => out.inspect if out[:exitcode] != 0 + end + + end #class + end +end diff --git a/plugins/communicators/winrm/shell.rb b/plugins/communicators/winrm/shell.rb index e1bbb5da0..63edb22af 100644 --- a/plugins/communicators/winrm/shell.rb +++ b/plugins/communicators/winrm/shell.rb @@ -9,6 +9,8 @@ Vagrant::Util::SilenceWarnings.silence! do require "winrm" end +require_relative "file_manager" + module VagrantPlugins module CommunicatorWinRM class WinRMShell @@ -62,31 +64,11 @@ module VagrantPlugins end def upload(from, to) - @logger.debug("Uploading: #{from} to #{to}") - file_name = (cmd("echo %TEMP%\\winrm-upload-#{rand()}"))[:data][0][:stdout].chomp - powershell <<-EOH - if(Test-Path #{to}) { - rm #{to} - } - EOH - Base64.encode64(IO.binread(from)).gsub("\n",'').chars.to_a.each_slice(8000-file_name.size) do |chunk| - out = cmd("echo #{chunk.join} >> \"#{file_name}\"") - end - powershell <<-EOH - mkdir $([System.IO.Path]::GetDirectoryName(\"#{to}\")) - $base64_string = Get-Content \"#{file_name}\" - $bytes = [System.Convert]::FromBase64String($base64_string) - $new_file = [System.IO.Path]::GetFullPath(\"#{to}\") - [System.IO.File]::WriteAllBytes($new_file,$bytes) - EOH + FileManager.new(self).upload(from, to) end def download(from, to) - @logger.debug("Downloading: #{from} to #{to}") - output = powershell("[System.convert]::ToBase64String([System.IO.File]::ReadAllBytes(\"#{from}\"))") - contents = output[:data].map!{|line| line[:stdout]}.join.gsub("\\n\\r", '') - out = Base64.decode64(contents) - IO.binwrite(to, out) + FileManager.new(self).download(from, to) end protected