Merge pull request #3519 from mitchellh/windows-guest-file-manager

communicators/winrm: Windows guest file manager
This commit is contained in:
Mitchell Hashimoto 2014-04-23 06:01:59 -07:00
commit fbcc6d25b4
4 changed files with 180 additions and 22 deletions

View File

@ -21,6 +21,10 @@ module VagrantPlugins
class WinRMNotReady < WinRMError
error_key(:winrm_not_ready)
end
class WinRMFileTransferError < WinRMError
error_key(:winrm_file_transfer_error)
end
end
end
end

View File

@ -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

View File

@ -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

View File

@ -20,3 +20,9 @@ en:
The box is not able to report an address for WinRM to connect to yet.
WinRM cannot access this Vagrant environment. Please wait for the
Vagrant environment to be running and try again.
winrm_file_transfer_error: |-
Failed to transfer a file between the host and guest
From: %{from}
To: %{to}
Message: %{message}