winrm-fs 0.2.3 → 0.3.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,166 @@
1
+ # -*- encoding: utf-8 -*-
2
+ #
3
+ # Author:: Fletcher (<fnichol@nichol.ca>)
4
+ #
5
+ # Copyright (C) 2015, Fletcher Nichol
6
+ #
7
+ # Licensed under the Apache License, Version 2.0 (the "License");
8
+ # you may not use this file except in compliance with the License.
9
+ # You may obtain a copy of the License at
10
+ #
11
+ # http://www.apache.org/licenses/LICENSE-2.0
12
+ #
13
+ # Unless required by applicable law or agreed to in writing, software
14
+ # distributed under the License is distributed on an "AS IS" BASIS,
15
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16
+ # See the License for the specific language governing permissions and
17
+ # limitations under the License.
18
+
19
+ require 'delegate'
20
+ require 'pathname'
21
+ require 'tempfile'
22
+ require 'zip'
23
+
24
+ module WinRM
25
+ module FS
26
+ module Core
27
+ # A temporary Zip file for a given directory.
28
+ #
29
+ # @author Fletcher Nichol <fnichol@nichol.ca>
30
+ class TmpZip
31
+ # Contructs a new Zip file for the given directory.
32
+ #
33
+ # There are 2 ways to interpret the directory path:
34
+ #
35
+ # * If the directory has no path separator terminator, then the
36
+ # directory basename will be used as the base directory in the
37
+ # resulting zip file.
38
+ # * If the directory has a path separator terminator (such as `/` or
39
+ # `\\`), then the entries under the directory will be added to the
40
+ # resulting zip file.
41
+ #
42
+ # The following emaples assume a directory tree structure of:
43
+ #
44
+ # src
45
+ # |-- alpha.txt
46
+ # |-- beta.txt
47
+ # \-- sub
48
+ # \-- charlie.txt
49
+ #
50
+ # @example Including the base directory in the zip file
51
+ #
52
+ # TmpZip.new("/path/to/src")
53
+ # # produces a zip file with entries:
54
+ # # - src/alpha.txt
55
+ # # - src/beta.txt
56
+ # # - src/sub/charlie.txt
57
+ #
58
+ # @example Excluding the base directory in the zip file
59
+ #
60
+ # TmpZip.new("/path/to/src/")
61
+ # # produces a zip file with entries:
62
+ # # - alpha.txt
63
+ # # - beta.txt
64
+ # # - sub/charlie.txt
65
+ #
66
+ # @param dir [String,Pathname,#to_s] path to the directory
67
+ # @param logger [#debug,#debug?] an optional logger/ui object that
68
+ # responds to `#debug` and `#debug?` (default `nil`)
69
+ def initialize(dir, logger = nil)
70
+ @logger = logger || Logging.logger[self]
71
+ @dir = Pathname.new(dir)
72
+ @zip_io = Tempfile.open(['tmpzip-', '.zip'], binmode: true)
73
+ write_zip
74
+ @zip_io.close
75
+ end
76
+
77
+ # @return [Pathname] path to zip file
78
+ def path
79
+ Pathname.new(zip_io.path) if zip_io.path
80
+ end
81
+
82
+ # Unlinks (deletes) the zip file from the filesystem.
83
+ def unlink
84
+ zip_io.unlink
85
+ end
86
+
87
+ private
88
+
89
+ # @return [Pathname] the directory used to create the Zip file
90
+ # @api private
91
+ attr_reader :dir
92
+
93
+ # @return [#debug] the logger
94
+ # @api private
95
+ attr_reader :logger
96
+
97
+ # @return [IO] the Zip file IO
98
+ # @api private
99
+ attr_reader :zip_io
100
+
101
+ # @return [Array<Pathname] all recursive files under the base
102
+ # directory, excluding directories
103
+ # @api private
104
+ def entries
105
+ Pathname.glob(dir.join('**/*')).delete_if(&:directory?).sort
106
+ end
107
+
108
+ # (see Logging.log_subject)
109
+ # @api private
110
+ def log_subject
111
+ @log_subject ||= [self.class.to_s.split('::').last, path].join('::')
112
+ end
113
+
114
+ # Adds all file entries to the Zip output stream.
115
+ #
116
+ # @param zos [Zip::OutputStream] zip output stream
117
+ # @api private
118
+ def produce_zip_entries(zos)
119
+ entries.each do |entry|
120
+ entry_path = entry.sub(/#{dir}\//i, '')
121
+ logger.debug "+++ Adding #{entry_path}"
122
+ zos.put_next_entry(
123
+ zip_entry(entry_path),
124
+ nil, nil, ::Zip::Entry::DEFLATED, Zlib::BEST_COMPRESSION)
125
+ entry.open('rb') { |src| IO.copy_stream(src, zos) }
126
+ end
127
+ logger.debug '=== All files added.'
128
+ end
129
+
130
+ # Writes out a temporary Zip file.
131
+ #
132
+ # @api private
133
+ def write_zip
134
+ logger.debug 'Populating files'
135
+ Zip::OutputStream.write_buffer(NoDupIO.new(zip_io)) do |zos|
136
+ produce_zip_entries(zos)
137
+ end
138
+ end
139
+
140
+ def zip_entry(entry_path)
141
+ Zip::Entry.new(
142
+ zip_io.path,
143
+ entry_path.to_s,
144
+ nil, nil, nil, nil, nil, nil,
145
+ ::Zip::DOSTime.new(2000)
146
+ )
147
+ end
148
+
149
+ # Simple delegate wrapper to prevent `#dup` calls being made on IO
150
+ # objects. This is used to bypass an issue in the `Zip::Outputstream`
151
+ # constructor where an incoming IO is duplicated, leading to races
152
+ # on flushing the final stream to disk.
153
+ #
154
+ # @author Fletcher Nichol <fnichol@nichol.ca>
155
+ # @api private
156
+ class NoDupIO < SimpleDelegator
157
+ # @return [self] returns self and does *not* return a duplicate
158
+ # object
159
+ def dup
160
+ self
161
+ end
162
+ end
163
+ end
164
+ end
165
+ end
166
+ end
@@ -14,8 +14,9 @@
14
14
  # See the License for the specific language governing permissions and
15
15
  # limitations under the License.
16
16
 
17
+ require 'winrm'
17
18
  require_relative 'scripts/scripts'
18
- require_relative 'core/upload_orchestrator'
19
+ require_relative 'core/file_transporter'
19
20
 
20
21
  module WinRM
21
22
  module FS
@@ -25,7 +26,7 @@ module WinRM
25
26
  # @param [WinRMWebService] WinRM web service client
26
27
  def initialize(service)
27
28
  @service = service
28
- @logger = Logging.logger[self]
29
+ @logger = service.logger
29
30
  end
30
31
 
31
32
  # Gets the MD5 checksum of the specified file if it exists,
@@ -34,7 +35,7 @@ module WinRM
34
35
  def checksum(path)
35
36
  @logger.debug("checksum: #{path}")
36
37
  script = WinRM::FS::Scripts.render('checksum', path: path)
37
- @service.powershell(script).stdout.chomp
38
+ @service.create_executor { |e| e.run_powershell_script(script).stdout.chomp }
38
39
  end
39
40
 
40
41
  # Create the specifed directory recursively
@@ -43,7 +44,7 @@ module WinRM
43
44
  def create_dir(path)
44
45
  @logger.debug("create_dir: #{path}")
45
46
  script = WinRM::FS::Scripts.render('create_dir', path: path)
46
- @service.powershell(script)[:exitcode] == 0
47
+ @service.create_executor { |e| e.run_powershell_script(script)[:exitcode] == 0 }
47
48
  end
48
49
 
49
50
  # Deletes the file or directory at the specified path
@@ -52,7 +53,7 @@ module WinRM
52
53
  def delete(path)
53
54
  @logger.debug("deleting: #{path}")
54
55
  script = WinRM::FS::Scripts.render('delete', path: path)
55
- @service.powershell(script)[:exitcode] == 0
56
+ @service.create_executor { |e| e.run_powershell_script(script)[:exitcode] == 0 }
56
57
  end
57
58
 
58
59
  # Downloads the specified remote file to the specified local path
@@ -61,7 +62,7 @@ module WinRM
61
62
  def download(remote_path, local_path)
62
63
  @logger.debug("downloading: #{remote_path} -> #{local_path}")
63
64
  script = WinRM::FS::Scripts.render('download', path: remote_path)
64
- output = @service.powershell(script)
65
+ output = @service.create_executor { |e| e.run_powershell_script(script) }
65
66
  return false if output[:exitcode] != 0
66
67
  contents = output.stdout.gsub('\n\r', '')
67
68
  out = Base64.decode64(contents)
@@ -75,14 +76,16 @@ module WinRM
75
76
  def exists?(path)
76
77
  @logger.debug("exists?: #{path}")
77
78
  script = WinRM::FS::Scripts.render('exists', path: path)
78
- @service.powershell(script)[:exitcode] == 0
79
+ @service.create_executor { |e| e.run_powershell_script(script)[:exitcode] == 0 }
79
80
  end
80
81
 
81
82
  # Gets the current user's TEMP directory on the remote system, for example
82
83
  # 'C:/Windows/Temp'
83
84
  # @return [String] Full path to the temp directory
84
85
  def temp_dir
85
- @guest_temp ||= (@service.cmd('echo %TEMP%')).stdout.chomp.gsub('\\', '/')
86
+ @guest_temp ||= begin
87
+ (@service.create_executor { |e| e.run_cmd('echo %TEMP%') }).stdout.chomp.gsub('\\', '/')
88
+ end
86
89
  end
87
90
 
88
91
  # Upload one or more local files and directories to a remote directory
@@ -104,13 +107,9 @@ module WinRM
104
107
  # @yieldparam [String] Target path on the winrm endpoint
105
108
  # @return [Fixnum] The total number of bytes copied
106
109
  def upload(local_path, remote_path, &block)
107
- @logger.debug("uploading: #{local_path} -> #{remote_path}")
108
-
109
- upload_orchestrator = WinRM::FS::Core::UploadOrchestrator.new(@service)
110
- if File.file?(local_path)
111
- upload_orchestrator.upload_file(local_path, remote_path, &block)
112
- else
113
- upload_orchestrator.upload_directory(local_path, remote_path, &block)
110
+ @service.create_executor do |executor|
111
+ file_transporter ||= WinRM::FS::Core::FileTransporter.new(executor)
112
+ file_transporter.upload(local_path, remote_path, &block)[0]
114
113
  end
115
114
  end
116
115
  end
@@ -0,0 +1,48 @@
1
+ $hash_file = "<%= hash_file %>"
2
+
3
+ Function Cleanup($o) { if (($o -ne $null) -and ($o.GetType().GetMethod("Dispose") -ne $null)) { $o.Dispose() } }
4
+
5
+ Function Decode-Base64File($src, $dst) {
6
+ Try {
7
+ $in = (Get-Item $src).OpenRead()
8
+ $b64 = New-Object -TypeName System.Security.Cryptography.FromBase64Transform
9
+ $m = [System.Security.Cryptography.CryptoStreamMode]::Read
10
+ $d = New-Object -TypeName System.Security.Cryptography.CryptoStream $in,$b64,$m
11
+ Copy-Stream $d ($out = [System.IO.File]::OpenWrite($dst))
12
+ } Finally { Cleanup $in; Cleanup $out; Cleanup $d }
13
+ }
14
+
15
+ Function Copy-Stream($src, $dst) { $b = New-Object Byte[] 4096; while (($i = $src.Read($b, 0, $b.Length)) -ne 0) { $dst.Write($b, 0, $i) } }
16
+
17
+ Function Check-Files($h) {
18
+ return $h.GetEnumerator() | ForEach-Object {
19
+ $dst = Unresolve-Path $_.Key
20
+ New-Object psobject -Property @{
21
+ chk_exists = ($exists = Test-Path $dst -PathType Leaf)
22
+ src_md5 = ($sMd5 = $_.Value)
23
+ dst_md5 = ($dMd5 = if ($exists) { Get-MD5Sum $dst } else { $null })
24
+ chk_dirty = ($dirty = if ($sMd5 -ne $dMd5) { $true } else { $false })
25
+ verifies = if ($dirty -eq $false) { $true } else { $false }
26
+ }
27
+ } | Select-Object -Property chk_exists,src_md5,dst_md5,chk_dirty,verifies
28
+ }
29
+
30
+ Function Get-MD5Sum($src) {
31
+ Try {
32
+ $c = New-Object -TypeName System.Security.Cryptography.MD5CryptoServiceProvider
33
+ $bytes = $c.ComputeHash(($in = (Get-Item $src).OpenRead()))
34
+ return ([System.BitConverter]::ToString($bytes)).Replace("-", "").ToLower()
35
+ } Finally { Cleanup $c; Cleanup $in }
36
+ }
37
+
38
+ Function Invoke-Input($in) {
39
+ $in = Unresolve-Path $in
40
+ Decode-Base64File $in ($decoded = "$($in).ps1")
41
+ $expr = Get-Content $decoded | Out-String
42
+ Remove-Item $in,$decoded -Force
43
+ return Invoke-Expression "$expr"
44
+ }
45
+
46
+ Function Unresolve-Path($p) { if ($p -eq $null) { return $null } else { return $ExecutionContext.SessionState.Path.GetUnresolvedProviderPathFromPSPath($p) } }
47
+
48
+ Check-Files (Invoke-Input $hash_file) | ConvertTo-Csv -NoTypeInformation
@@ -0,0 +1,59 @@
1
+ trap {$e = $_.Exception; $e.InvocationInfo.ScriptName; do {$e.Message; $e = $e.InnerException} while ($e); break;}
2
+ $progresspreference = 'SilentlyContinue'
3
+ function Decode-Base64File($src, $dst) {folder (split-path $dst);sc -force -Encoding Byte -Path $dst -Value ([Convert]::FromBase64String([IO.File]::ReadAllLines($src)))}
4
+ function Copy-Stream($src, $dst) {$b = New-Object Byte[] 4096; while (($i = $src.Read($b, 0, $b.Length)) -ne 0) { $dst.Write($b, 0, $i) } }
5
+ function Resolve-ProviderPath{ $input | % {if ($_){(Resolve-Path $_).ProviderPath} else{$null}} }
6
+ function Get-FrameworkVersion { "Full", "Client" | % {([version](gp "HKLM:\Software\Microsoft\NET Framework Setup\NDP\v4\$_").Version)} | select -first 1}
7
+ function Test-NETStack($Version){ Get-FrameworkVersion -ge $Version }
8
+ function Test-IOCompression {($PSVersionTable.PSVersion.Major -ge 3) -and (Test-NETStack '4.5')}
9
+ function folder($path){ $path | ? {-not (test-path $_)} | % {$null = mkdir $_}}
10
+ function disposable($o){($o -is [IDisposable]) -and (($o | gm | %{$_.name}) -contains 'Dispose')}
11
+ function use($obj, [scriptblock]$sb){try {& $sb} catch [exception]{throw $_} finally {if (disposable $obj) {$obj.Dispose()}} }
12
+ set-alias RPP -Value Resolve-ProviderPath
13
+
14
+ Function Decode-Files($hash) {
15
+ foreach ($key in $hash.keys) {
16
+ $value = $hash[$key]
17
+ $tmp, $tzip, $dst = $Key, $Value["tmpzip"], $Value["dst"]
18
+ if($tmp -ne "") {
19
+ $sMd5 = (gi $tmp).BaseName.Replace("b64-", "")
20
+ $decoded = if ($tzip -ne $null) { $tzip } else { $dst }
21
+ Decode-Base64File $tmp $decoded
22
+ rm $tmp -Force
23
+ $dMd5 = Get-MD5Sum $decoded
24
+ $verifies = $sMd5 -like $dMd5
25
+ }
26
+ if ($tzip) {Unzip-File $tzip $dst}
27
+ New-Object psobject -Property @{dst=$dst;verifies=$verifies;src_md5=$sMd5;dst_md5=$dMd5;tmpfile=$tmp;tmpzip=$tzip}
28
+ }
29
+ }
30
+
31
+ Function Get-MD5Sum($src) {
32
+ if ($src -and (test-path $src)) {
33
+ use ($c = New-Object -TypeName Security.Cryptography.MD5CryptoServiceProvider) {
34
+ use ($in = (gi $src).OpenRead()) {([BitConverter]::ToString($c.ComputeHash($in))).Replace("-", "").ToLower()}}
35
+ }
36
+ }
37
+
38
+ Function Invoke-Input($in) {
39
+ $in = $in | rpp
40
+ $d = "$($in).ps1"
41
+ Decode-Base64File $in $d
42
+ $expr = gc $d | Out-String
43
+ rm $in,$d -Force
44
+ iex "$expr"
45
+ }
46
+
47
+ Function Unzip-File($src, $dst) {
48
+ $unpack = $src -replace '\.zip'
49
+ $dst_parent = Split-Path -Path $dst -Parent
50
+ if(!(Test-Path $dst_parent)) { $dst = $dst_parent }
51
+ folder $unpack, $dst
52
+ if (Test-IOCompression) {Add-Type -AN System.IO.Compression.FileSystem; [IO.Compression.ZipFile]::ExtractToDirectory($src, $unpack)}
53
+ else {Try {$s = New-Object -ComObject Shell.Application; ($s.NameSpace($unpack)).CopyHere(($s.NameSpace($src)).Items(), 0x610)} Finally {[void][Runtime.Interopservices.Marshal]::ReleaseComObject($s)}}
54
+ dir $unpack | cp -dest "$dst/" -force -recurse
55
+ rm $unpack -recurse -force
56
+ }
57
+
58
+ $hash_file = "<%= hash_file %>"
59
+ Decode-Files (Invoke-Input $hash_file) | ConvertTo-Csv -NoTypeInformation
@@ -1,7 +1,8 @@
1
1
  $p = $ExecutionContext.SessionState.Path
2
2
  $path = $p.GetUnresolvedProviderPathFromPSPath("<%= path %>")
3
3
  if (Test-Path $path -PathType Leaf) {
4
- [System.convert]::ToBase64String([System.IO.File]::ReadAllBytes($path))
4
+ $bytes = [System.convert]::ToBase64String([System.IO.File]::ReadAllBytes($path))
5
+ Write-Host $bytes
5
6
  exit 0
6
7
  }
7
8
  exit 1
@@ -1,5 +1,5 @@
1
1
  auth_type: plaintext
2
- endpoint: "http://localhost:55985/wsman"
2
+ endpoint: "http://192.168.137.20:5985/wsman"
3
3
  options:
4
4
  user: vagrant
5
5
  pass: vagrant
@@ -1,10 +1,10 @@
1
1
  # encoding: UTF-8
2
2
  require 'pathname'
3
3
 
4
- describe WinRM::FS::FileManager, integration: true do
4
+ describe WinRM::FS::FileManager do
5
5
  let(:dest_dir) { File.join(subject.temp_dir, "winrm_#{rand(2**16)}") }
6
6
  let(:temp_upload_dir) { '$env:TEMP/winrm-upload' }
7
- let(:this_dir) { File.expand_path(File.dirname(__FILE__)) }
7
+ let(:spec_dir) { File.expand_path(File.dirname(File.dirname(__FILE__))) }
8
8
  let(:this_file) { __FILE__ }
9
9
  let(:service) { winrm_connection }
10
10
 
@@ -35,7 +35,7 @@ describe WinRM::FS::FileManager, integration: true do
35
35
 
36
36
  context 'temp_dir' do
37
37
  it 'should return the remote users temp dir' do
38
- expect(subject.temp_dir).to match(%r{C:/Users/\w+/AppData/Local/Temp})
38
+ expect(subject.temp_dir).to match(%r{C:/Users/\S+/AppData/Local/Temp})
39
39
  end
40
40
  end
41
41
 
@@ -58,7 +58,7 @@ describe WinRM::FS::FileManager, integration: true do
58
58
  end
59
59
 
60
60
  it 'should upload using relative file path' do
61
- subject.upload('./spec/file_manager_spec.rb', dest_file)
61
+ subject.upload('./spec/integration/file_manager_spec.rb', dest_file)
62
62
  expect(subject).to have_created(dest_file).with_content(this_file)
63
63
  end
64
64
 
@@ -88,14 +88,16 @@ describe WinRM::FS::FileManager, integration: true do
88
88
 
89
89
  it 'yields progress data' do
90
90
  block_called = false
91
+ total_bytes_copied = 0
91
92
  total = subject.upload(this_file, dest_file) do \
92
93
  |bytes_copied, total_bytes, local_path, remote_path|
93
94
  expect(total_bytes).to be > 0
94
- expect(bytes_copied).to eq(total_bytes)
95
+ total_bytes_copied = bytes_copied
95
96
  expect(local_path).to eq(this_file)
96
97
  expect(remote_path).to eq(dest_file)
97
98
  block_called = true
98
99
  end
100
+ expect(total_bytes_copied).to eq(total)
99
101
  expect(block_called).to be true
100
102
  expect(total).to be > 0
101
103
  end
@@ -107,7 +109,7 @@ describe WinRM::FS::FileManager, integration: true do
107
109
  end
108
110
 
109
111
  it 'should upload when content differs' do
110
- matchers_file = File.join(this_dir, 'matchers.rb')
112
+ matchers_file = File.join(spec_dir, 'matchers.rb')
111
113
  subject.upload(matchers_file, dest_file)
112
114
  bytes_uploaded = subject.upload(this_file, dest_file)
113
115
  expect(bytes_uploaded).to be > 0
@@ -119,32 +121,48 @@ describe WinRM::FS::FileManager, integration: true do
119
121
  end
120
122
 
121
123
  context 'upload empty file' do
122
- let(:empty_src_file) { Tempfile.new('empty').path }
124
+ let(:empty_src_file) { Tempfile.new('empty') }
123
125
  let(:dest_file) { File.join(dest_dir, 'emptyfile.txt') }
124
126
 
125
127
  it 'creates a new empty file' do
126
- expect(subject.upload(empty_src_file, dest_file)).to be 0
128
+ expect(subject.upload(empty_src_file.path, dest_file)).to be 0
127
129
  expect(subject).to have_created(dest_file).with_content('')
128
130
  end
129
131
 
130
132
  it 'overwrites an existing file' do
131
133
  expect(subject.upload(this_file, dest_file)).to be > 0
132
- expect(subject.upload(empty_src_file, dest_file)).to be 0
134
+ expect(subject.upload(empty_src_file.path, dest_file)).to be 0
133
135
  expect(subject).to have_created(dest_file).with_content('')
134
136
  end
135
137
  end
136
138
 
137
139
  context 'upload directory' do
138
- let(:root_dir) { File.expand_path('../', File.dirname(__FILE__)) }
140
+ let(:root_dir) { File.expand_path('../../', File.dirname(__FILE__)) }
139
141
  let(:winrm_fs_dir) { File.join(root_dir, 'lib/winrm-fs') }
140
142
  let(:core_dir) { File.join(root_dir, 'lib/winrm-fs/core') }
141
143
 
142
- it 'copies the entire directory recursively' do
144
+ it 'copies the directory contents recursively when directory does not exist' do
143
145
  bytes_uploaded = subject.upload(winrm_fs_dir, dest_dir)
144
146
  expect(bytes_uploaded).to be > 0
145
147
 
146
148
  Dir.glob(winrm_fs_dir + '/**/*.rb').each do |host_file|
147
- host_file_rel = Pathname.new(host_file).relative_path_from(Pathname.new(winrm_fs_dir)).to_s
149
+ host_file_rel = Pathname.new(host_file).relative_path_from(
150
+ Pathname.new(winrm_fs_dir)
151
+ ).to_s
152
+ remote_file = File.join(dest_dir, host_file_rel)
153
+ expect(subject).to have_created(remote_file).with_content(host_file)
154
+ end
155
+ end
156
+
157
+ it 'copies the directory recursively when directory does exist' do
158
+ subject.create_dir(dest_dir)
159
+ bytes_uploaded = subject.upload(winrm_fs_dir, dest_dir)
160
+ expect(bytes_uploaded).to be > 0
161
+
162
+ Dir.glob(winrm_fs_dir + '/**/*.rb').each do |host_file|
163
+ host_file_rel = Pathname.new(host_file).relative_path_from(
164
+ Pathname.new(winrm_fs_dir).dirname
165
+ ).to_s
148
166
  remote_file = File.join(dest_dir, host_file_rel)
149
167
  expect(subject).to have_created(remote_file).with_content(host_file)
150
168
  end
@@ -156,6 +174,14 @@ describe WinRM::FS::FileManager, integration: true do
156
174
  expect(bytes_uploaded).to eq 0
157
175
  end
158
176
 
177
+ it 'unzips the directory when cached content is the same' do
178
+ subject.upload(winrm_fs_dir, dest_dir)
179
+ subject.delete(dest_dir)
180
+ expect(subject.exists?(dest_dir)).to be false
181
+ subject.upload(winrm_fs_dir, dest_dir)
182
+ expect(subject.exists?(dest_dir)).to be true
183
+ end
184
+
159
185
  it 'copies the directory when content differs' do
160
186
  subject.upload(winrm_fs_dir, dest_dir)
161
187
  bytes_uploaded = subject.upload(core_dir, dest_dir)