winrm-fs 0.4.3 → 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 7cb6a97dc554ab9436a28969f3ea70c8771c0858
4
- data.tar.gz: 4578906a4b74cffeb2f00bfe362dd059a7b33db6
3
+ metadata.gz: 3e54c394c912274542248ea943f17cbf8b5801b7
4
+ data.tar.gz: 8fa0f2fead6f018f6f22cde2b1109c6fcda72fde
5
5
  SHA512:
6
- metadata.gz: babcce6f4e58ad60de51a34844149c551edcc61201b1f358472b2796b2d4a91759ae7d0ae878afb88f20a500f1e4928132dcbf0997ddcadd9299c514ee2a0811
7
- data.tar.gz: c92f3204daef1eb4d47d56f174651adb1640b57bd17664518f05ffd7d795738942fd8c1f8748d3c1fbb98a3f745dd289844fe8dfeed7d7198696ac3b1f38e84c
6
+ metadata.gz: 3d11d2a9ea8715736c3dd9caedfc01ef96a954043896f15e9cd7e164f45b2d8222fe6d0151895c39969e1864347a788009cdd4f6ae83e2041dab02f57deca55d
7
+ data.tar.gz: 74dda7732bd2094e698a6651ef1cfc83fd64969c9cba111e8c75dc536133209d8c453a2ebe7eba321a9d049ea9e44d04904b89ba27a14bb1439391149abd997e
data/.travis.yml CHANGED
@@ -1,5 +1,9 @@
1
1
  language: ruby
2
2
  rvm:
3
- - 1.9.3
4
3
  - 2.0.0
5
- - 2.1.0
4
+ - 2.1.0
5
+
6
+ # This prevents testing branches that are created just for PRs
7
+ branches:
8
+ only:
9
+ - master
data/Gemfile CHANGED
@@ -1,3 +1,5 @@
1
1
  # encoding: UTF-8
2
2
  source 'https://rubygems.org'
3
3
  gemspec
4
+
5
+ gem 'rb-readline'
data/README.md CHANGED
@@ -8,8 +8,8 @@ Files may be copied from the local machine to the winrm endpoint. Individual fil
8
8
  ```ruby
9
9
  require 'winrm-fs'
10
10
 
11
- service = WinRM::WinRMWebService.new(...
12
- file_manager = WinRM::FS::FileManager.new(service)
11
+ connection = WinRM::Connection.new(...
12
+ file_manager = WinRM::FS::FileManager.new(connection)
13
13
 
14
14
  # upload file.txt from the current working directory
15
15
  file_manager.upload('file.txt', 'c:/file.txt')
@@ -25,6 +25,9 @@ file_manager.upload([
25
25
  ], '$env:ProgramData')
26
26
  ```
27
27
 
28
+ ### Optimizing WinRM settings
29
+ Since winrm-fs 1.0/winrm 2.0, files are uploaded using the PSRP protocol and transfer speeds are dramatically improved from previous versions. This is largely due to the fact that the size of chunks that can be transferred at one time are now governed by the `MaxEnvelopeSizekb` winrm configuration setting on the endpoint. This default to 500 on Windows 2012 R2 and 150 on Windows 2008 R2. You may experience much faster transfer rates on 2008 R2 by increasing this setting.
30
+
28
31
  ### Handling progress events
29
32
  If you want to implement your own custom progress handling, you can pass a code
30
33
  block and use the proggress data that `upload` yields to this block:
@@ -40,16 +43,6 @@ If you're having trouble, first of all its most likely a network or WinRM config
40
43
  issue. Take a look at the [WinRM gem troubleshooting](https://github.com/WinRb/WinRM#troubleshooting)
41
44
  first.
42
45
 
43
- The most [common error](https://github.com/WinRb/winrm-fs/issues/1) with this gem is getting a 500 error because your maxConcurrentOperationsPerUser limit has been reached.
44
-
45
- ```
46
- The WS-Management service cannot process the request. This user is allowed a
47
- maximum number of 1500 concurrent operations, which has been exceeded. Close
48
- existing operations for this user, or raise the quota for this user.
49
- ```
50
-
51
- You can workaround this by increasing your operations per user quota.
52
-
53
46
  ## Contributing
54
47
 
55
48
  1. Fork it.
data/VERSION CHANGED
@@ -1 +1 @@
1
- 0.4.3
1
+ 1.0.0
data/Vagrantfile CHANGED
@@ -5,5 +5,5 @@
5
5
  VAGRANTFILE_API_VERSION = '2'
6
6
 
7
7
  Vagrant.configure(VAGRANTFILE_API_VERSION) do |config|
8
- config.vm.box = 'mwrock/Windows2012R2'
8
+ config.vm.box = 'mwrock/WindowsNano'
9
9
  end
data/appveyor.yml CHANGED
@@ -6,7 +6,7 @@ platform:
6
6
 
7
7
  environment:
8
8
  winrm_user: test_user
9
- winrm_pass: Pass@word1
9
+ winrm_password: Pass@word1
10
10
 
11
11
  matrix:
12
12
  - ruby_version: "21"
@@ -19,7 +19,7 @@ branches:
19
19
  - master
20
20
 
21
21
  install:
22
- - ps: net user /add $env:winrm_user $env:winrm_pass
22
+ - ps: net user /add $env:winrm_user $env:winrm_password
23
23
  - ps: net localgroup administrators $env:winrm_user /add
24
24
  - ps: winrm set winrm/config/client/auth '@{Basic="true"}'
25
25
  - ps: winrm set winrm/config/service/auth '@{Basic="true"}'
data/changelog.md CHANGED
@@ -1,4 +1,6 @@
1
1
  # WinRM-fs Gem Changelog
2
+ # 1.0.0
3
+ - Using winrm v2. File uploads just got a whole lot faster!
2
4
 
3
5
  # 0.4.3
4
6
  - Fix error handling with wmf5, filtering out progress output from inspected stderr.
@@ -41,8 +41,8 @@ module WinRM
41
41
  # sessions being invoked which can be 2 orders of magnitude more
42
42
  # expensive than vanilla CMD commands.
43
43
  #
44
- # This object is supported by either a `CommandExecutor` instance as it
45
- # depends on the `#run_cmd` and `#run_powershell_script` API contracts.
44
+ # This object is supported by a `PowerShell` instance as it
45
+ # depends on the `#run` API contract.
46
46
  #
47
47
  # An optional logger can be supplied, assuming it can respond to the
48
48
  # `#debug` and `#debug?` messages.
@@ -50,12 +50,12 @@ module WinRM
50
50
  # @author Fletcher Nichol <fnichol@nichol.ca>
51
51
  # @author Matt Wrock <matt@mattwrock.com>
52
52
  class FileTransporter
53
- # Creates a FileTransporter given a CommandExecutor object.
53
+ # Creates a FileTransporter given a PowerShell object.
54
54
  #
55
- # @param executor [CommandExecutor] a winrm CommandExecutor object
56
- def initialize(executor, opts = {})
57
- @executor = executor
58
- @logger = executor.service.logger
55
+ # @param shell [PowerShell] a winrm PowerShell object
56
+ def initialize(shell, opts = {})
57
+ @shell = shell
58
+ @logger = shell.logger
59
59
  @id_generator = opts.fetch(:id_generator) { -> { SecureRandom.uuid } }
60
60
  end
61
61
 
@@ -74,6 +74,7 @@ module WinRM
74
74
  def upload(locals, remote)
75
75
  files = nil
76
76
  report = nil
77
+ remote = remote.to_s
77
78
 
78
79
  elapsed1 = Benchmark.measure do
79
80
  files = make_files_hash(Array(locals), remote)
@@ -91,7 +92,7 @@ module WinRM
91
92
  end
92
93
 
93
94
  elapsed3 = Benchmark.measure do
94
- report = decode_files(files)
95
+ report = extract_files(files)
95
96
  merge_with_report!(files, report)
96
97
  cleanup(files)
97
98
  end
@@ -100,7 +101,7 @@ module WinRM
100
101
  "Uploaded #{files.keys.size} items " \
101
102
  "dirty_check: #{duration(elapsed1.real)} " \
102
103
  "stream_files: #{duration(elapsed2.real)} " \
103
- "decode: #{duration(elapsed3.real)} " \
104
+ "extract: #{duration(elapsed3.real)} " \
104
105
  )
105
106
 
106
107
  [total_size, files]
@@ -108,12 +109,6 @@ module WinRM
108
109
 
109
110
  private
110
111
 
111
- # @return [Integer] the maximum number of bytes that can be supplied on
112
- # a Windows CMD prompt without exceeded the maximum command line
113
- # length
114
- # @api private
115
- MAX_ENCODED_WRITE = 8000
116
-
117
112
  # @return [String] the Array pack template for Base64 encoding a stream
118
113
  # of data
119
114
  # @api private
@@ -128,9 +123,24 @@ module WinRM
128
123
  # @api private
129
124
  attr_reader :logger
130
125
 
131
- # @return [Winrm::CommandExecutor] a WinRM CommandExecutor
126
+ # @return [Winrm::Shells::Powershell] a WinRM Powershell shell
127
+ # @api private
128
+ attr_reader :shell
129
+
130
+ # @return [Integer] the maximum number of bytes to send per request
131
+ # when streaming a file. This is optimized to send as much data
132
+ # as allowed in a single PSRP fragment
132
133
  # @api private
133
- attr_reader :executor
134
+ def max_encoded_write
135
+ @max_encoded_write ||= begin
136
+ empty_command = WinRM::PSRP::MessageFactory.create_pipeline_message(
137
+ '00000000-0000-0000-0000-000000000000',
138
+ '00000000-0000-0000-0000-000000000000',
139
+ stream_command('')
140
+ )
141
+ shell.max_fragment_blob_size - empty_command.bytes.length
142
+ end
143
+ end
134
144
 
135
145
  # Examines the files and corrects the file destination if it is
136
146
  # targeting an existing folder. In this case, the destination path
@@ -198,9 +208,9 @@ module WinRM
198
208
  # @api private
199
209
  def check_files(files)
200
210
  logger.debug 'Running check_files.ps1'
201
- hash_file = create_remote_hash_file(check_files_ps_hash(files))
211
+ hash_file = check_files_ps_hash(files)
202
212
  script = WinRM::FS::Scripts.render('check_files', hash_file: hash_file)
203
- parse_response(executor.run_powershell_script(script))
213
+ parse_response(shell.run(script))
204
214
  end
205
215
 
206
216
  # Constructs a collection of destination path/MD5 checksum pairs as a
@@ -236,63 +246,44 @@ module WinRM
236
246
  end
237
247
  end
238
248
 
239
- # Creates a remote Base64-encoded temporary file containing a
240
- # PowerShell hash table.
241
- #
242
- # @param hash [String] a String representation of a PowerShell hash
243
- # table
244
- # @return [String] the remote path to the temporary file
245
- # @api private
246
- def create_remote_hash_file(hash)
247
- hash_file = "$env:TEMP\\hash-#{@id_generator.call}.txt"
248
- hash.lines.each { |line| logger.debug line.chomp }
249
- StringIO.open(hash) { |io| stream_upload(io, hash_file) }
250
- hash_file
251
- end
252
-
253
- # Runs the decode_files PowerShell script against a collection of
249
+ # Runs the extract_files PowerShell script against a collection of
254
250
  # temporary file/destination path pairs. The PowerShell script returns
255
251
  # its results as a CSV-formatted report which is converted into a Ruby
256
- # Hash. The script will not be invoked if there are no "dirty" files
252
+ # Hash. The script will not be invoked if there are no zip files
257
253
  # present in the incoming files Hash.
258
254
  #
259
255
  # @param files [Hash] files hash, keyed by the local MD5 digest
260
256
  # @return [Hash] a report hash, keyed by the local MD5 digest
261
257
  # @api private
262
- def decode_files(files)
263
- decoded_files = decode_files_ps_hash(files)
258
+ def extract_files(files)
259
+ extracted_files = extract_files_ps_hash(files)
264
260
 
265
- if decoded_files == ps_hash({})
266
- logger.debug 'No remote files to decode, skipping'
261
+ if extracted_files == ps_hash({})
262
+ logger.debug 'No remote files to extract, skipping'
267
263
  {}
268
264
  else
269
- logger.debug 'Running decode_files.ps1'
270
- hash_file = create_remote_hash_file(decoded_files)
271
- script = WinRM::FS::Scripts.render('decode_files', hash_file: hash_file)
265
+ logger.debug 'Running extract_files.ps1'
266
+ script = WinRM::FS::Scripts.render('extract_files', hash_file: extracted_files)
272
267
 
273
- parse_response(executor.run_powershell_script(script))
268
+ parse_response(shell.run(script))
274
269
  end
275
270
  end
276
271
 
277
272
  # Constructs a collection of temporary file/destination path pairs for
278
- # all "dirty" files as a String representation of the contents of a
279
- # PowerShell Hash Table. A "dirty" file is one which has the
280
- # `"chk_dirty"` option set to `"True"` in the incoming files Hash.
273
+ # all zipped folders as a String representation of the contents of a
274
+ # PowerShell Hash Table.
281
275
  #
282
276
  # @param files [Hash] files hash, keyed by the local MD5 digest
283
277
  # @return [String] the inner contents of a PowerShell Hash Table
284
278
  # @api private
285
- def decode_files_ps_hash(files)
286
- file_data = files.select do |_, data|
287
- data['chk_dirty'] == 'True' || data.key?('tmpzip')
288
- end
279
+ def extract_files_ps_hash(files)
280
+ file_data = files.select { |_, data| data.key?('tmpzip') }
289
281
 
290
- i = 0
291
- result = file_data.map do |_, data|
282
+ result = file_data.map do |md5, data|
292
283
  val = { 'dst' => data['dst'] }
293
284
  val['tmpzip'] = data['tmpzip'] if data['tmpzip']
294
285
 
295
- [data['tmpfile'] || "clean#{i += 1}", val]
286
+ [md5, val]
296
287
  end
297
288
 
298
289
  ps_hash(Hash[result])
@@ -321,6 +312,7 @@ module WinRM
321
312
  def make_files_hash(locals, remote)
322
313
  hash = {}
323
314
  locals.each do |local|
315
+ local = local.to_s
324
316
  expanded = File.expand_path(local)
325
317
  expanded += local[-1] if local.end_with?('/', '\\')
326
318
 
@@ -358,20 +350,6 @@ module WinRM
358
350
  ' ' * depth
359
351
  end
360
352
 
361
- # Parses CLIXML String into regular String (without any XML syntax).
362
- # Inspired by https://github.com/WinRb/WinRM/issues/106.
363
- #
364
- # @param clixml [String] clixml text
365
- # @return [String] parsed clixml into String
366
- def clixml_to_s(clixml)
367
- doc = REXML::Document.new(clixml)
368
- text = doc.get_elements('//S').map(&:text).join
369
- text.gsub(/_x(\h\h\h\h)_/) do
370
- code = Regexp.last_match[1]
371
- code.hex.chr
372
- end
373
- end
374
-
375
353
  # Parses response of a PowerShell script or CMD command which contains
376
354
  # a CSV-formatted document in the standard output stream.
377
355
  #
@@ -380,22 +358,15 @@ module WinRM
380
358
  # @return [Hash] report hash, keyed by the local MD5 digest
381
359
  # @api private
382
360
  def parse_response(output)
383
- exitcode = output[:exitcode]
361
+ exitcode = output.exitcode
384
362
  stderr = output.stderr
385
- if stderr.include?('The command line is too long')
386
- # The powershell script which should result in `output` parameter
387
- # is too long, remove some newlines, comments, etc from it.
388
- fail StandardError, 'The command line is too long' \
389
- ' (powershell script is too long)'
390
- end
391
- pretty_stderr = clixml_to_s(stderr)
392
363
 
393
364
  if exitcode != 0
394
365
  fail FileTransporterFailed, "[#{self.class}] Upload failed " \
395
- "(exitcode: #{exitcode})\n#{pretty_stderr}"
396
- elsif pretty_stderr != '\r\n' && pretty_stderr != ''
366
+ "(exitcode: #{exitcode})\n#{stderr}"
367
+ elsif stderr != '\r\n' && stderr != ''
397
368
  fail FileTransporterFailed, "[#{self.class}] Upload failed " \
398
- "(exitcode: 0), but stderr present\n#{pretty_stderr}"
369
+ "(exitcode: 0), but stderr present\n#{stderr}"
399
370
  end
400
371
 
401
372
  logger.debug 'Parsing CSV Response'
@@ -441,42 +412,59 @@ module WinRM
441
412
  # the number of bytes transferred to the remote host
442
413
  # @api private
443
414
  def stream_upload(input_io, dest)
444
- dest_cmd = dest.sub('$env:TEMP', '%TEMP%')
445
- read_size = (MAX_ENCODED_WRITE.to_i / 4) * 3
415
+ read_size = ((max_encoded_write - dest.length) / 4) * 3
446
416
  chunk, bytes = 1, 0
447
417
  buffer = ''
448
- executor.run_cmd(%(echo|set /p=>"#{dest_cmd}")) # truncate empty file
418
+ shell.run(<<-EOS
419
+ $to = $ExecutionContext.SessionState.Path.GetUnresolvedProviderPathFromPSPath("#{dest}")
420
+ $parent = Split-Path $to
421
+ if(!(Test-path $parent)) { mkdir $parent | Out-Null }
422
+ $fileStream = New-Object -TypeName System.IO.FileStream -ArgumentList @(
423
+ $to,
424
+ [system.io.filemode]::Create,
425
+ [System.io.FileAccess]::Write,
426
+ [System.IO.FileShare]::ReadWrite
427
+ )
428
+ EOS
429
+ )
430
+
449
431
  while input_io.read(read_size, buffer)
450
432
  bytes += (buffer.bytesize / 3 * 4)
451
- executor.run_cmd([buffer].pack(BASE64_PACK)
452
- .insert(0, 'echo ').concat(%( >> "#{dest_cmd}")))
433
+ shell.run(stream_command([buffer].pack(BASE64_PACK)))
453
434
  logger.debug "Wrote chunk #{chunk} for #{dest}" if chunk % 25 == 0
454
435
  chunk += 1
455
436
  yield bytes if block_given?
456
437
  end
438
+ shell.run('$fileStream.Dispose()')
457
439
  buffer = nil # rubocop:disable Lint/UselessAssignment
458
440
 
459
441
  [chunk - 1, bytes]
460
442
  end
461
443
 
462
- # Uploads a local file to a Base64-encoded temporary file.
444
+ def stream_command(encoded_bytes)
445
+ <<-EOS
446
+ $bytes=[Convert]::FromBase64String('#{encoded_bytes}')
447
+ $fileStream.Write($bytes, 0, $bytes.length)
448
+ EOS
449
+ end
450
+
451
+ # Uploads a local file.
463
452
  #
464
453
  # @param src [String] path to a local file
465
- # @param tmpfile [String] path to the temporary file on the remote
466
- # host
454
+ # @param dest [String] path to the file on the remote host
467
455
  # @return [Integer,Integer] the number of resulting upload chunks and
468
456
  # the number of bytes transferred to the remote host
469
457
  # @api private
470
- def stream_upload_file(src, tmpfile, &block)
471
- logger.debug "Uploading #{src} to encoded tmpfile #{tmpfile}"
458
+ def stream_upload_file(src, dest, &block)
459
+ logger.debug "Uploading #{src} to #{dest}"
472
460
  chunks, bytes = 0, 0
473
461
  elapsed = Benchmark.measure do
474
462
  File.open(src, 'rb') do |io|
475
- chunks, bytes = stream_upload(io, tmpfile, &block)
463
+ chunks, bytes = stream_upload(io, dest, &block)
476
464
  end
477
465
  end
478
466
  logger.debug(
479
- "Finished uploading #{src} to encoded tmpfile #{tmpfile} " \
467
+ "Finished uploading #{src} to #{dest} " \
480
468
  "(#{bytes.to_f / 1000} KB over #{chunks} chunks) " \
481
469
  "in #{duration(elapsed.real)}"
482
470
  )
@@ -496,9 +484,8 @@ module WinRM
496
484
  files.each do |md5, data|
497
485
  src = data.fetch('src_zip', data['src'])
498
486
  if data['chk_dirty'] == 'True'
499
- tmpfile = "$env:TEMP\\b64-#{md5}.txt"
500
- response[md5] = { 'tmpfile' => tmpfile }
501
- chunks, bytes = stream_upload_file(src, tmpfile) do |xfered|
487
+ response[md5] = { 'dest' => data['tmpzip'] || data['dst'] }
488
+ chunks, bytes = stream_upload_file(src, data['tmpzip'] || data['dst']) do |xfered|
502
489
  yield data['src'], xfered
503
490
  end
504
491
  response[md5]['chunks'] = chunks
@@ -23,10 +23,10 @@ module WinRM
23
23
  # Perform file transfer operations between a local machine and winrm endpoint
24
24
  class FileManager
25
25
  # Creates a new FileManager instance
26
- # @param [WinRMWebService] WinRM web service client
27
- def initialize(service)
28
- @service = service
29
- @logger = service.logger
26
+ # @param [WinRM::Connection] WinRM web connection client
27
+ def initialize(connection)
28
+ @connection = connection
29
+ @logger = connection.logger
30
30
  end
31
31
 
32
32
  # Gets the MD5 checksum of the specified file if it exists,
@@ -35,7 +35,7 @@ module WinRM
35
35
  def checksum(path)
36
36
  @logger.debug("checksum: #{path}")
37
37
  script = WinRM::FS::Scripts.render('checksum', path: path)
38
- @service.create_executor { |e| e.run_powershell_script(script).stdout.chomp }
38
+ @connection.shell(:powershell) { |e| e.run(script).stdout.chomp }
39
39
  end
40
40
 
41
41
  # Create the specifed directory recursively
@@ -44,7 +44,7 @@ module WinRM
44
44
  def create_dir(path)
45
45
  @logger.debug("create_dir: #{path}")
46
46
  script = WinRM::FS::Scripts.render('create_dir', path: path)
47
- @service.create_executor { |e| e.run_powershell_script(script)[:exitcode] == 0 }
47
+ @connection.shell(:powershell) { |e| e.run(script).exitcode == 0 }
48
48
  end
49
49
 
50
50
  # Deletes the file or directory at the specified path
@@ -53,7 +53,7 @@ module WinRM
53
53
  def delete(path)
54
54
  @logger.debug("deleting: #{path}")
55
55
  script = WinRM::FS::Scripts.render('delete', path: path)
56
- @service.create_executor { |e| e.run_powershell_script(script)[:exitcode] == 0 }
56
+ @connection.shell(:powershell) { |e| e.run(script).exitcode == 0 }
57
57
  end
58
58
 
59
59
  # Downloads the specified remote file to the specified local path
@@ -62,8 +62,8 @@ module WinRM
62
62
  def download(remote_path, local_path)
63
63
  @logger.debug("downloading: #{remote_path} -> #{local_path}")
64
64
  script = WinRM::FS::Scripts.render('download', path: remote_path)
65
- output = @service.create_executor { |e| e.run_powershell_script(script) }
66
- return false if output[:exitcode] != 0
65
+ output = @connection.shell(:powershell) { |e| e.run(script) }
66
+ return false if output.exitcode != 0
67
67
  contents = output.stdout.gsub('\n\r', '')
68
68
  out = Base64.decode64(contents)
69
69
  IO.binwrite(local_path, out)
@@ -76,7 +76,7 @@ module WinRM
76
76
  def exists?(path)
77
77
  @logger.debug("exists?: #{path}")
78
78
  script = WinRM::FS::Scripts.render('exists', path: path)
79
- @service.create_executor { |e| e.run_powershell_script(script)[:exitcode] == 0 }
79
+ @connection.shell(:powershell) { |e| e.run(script).exitcode == 0 }
80
80
  end
81
81
 
82
82
  # Gets the current user's TEMP directory on the remote system, for example
@@ -84,7 +84,7 @@ module WinRM
84
84
  # @return [String] Full path to the temp directory
85
85
  def temp_dir
86
86
  @guest_temp ||= begin
87
- (@service.create_executor { |e| e.run_cmd('echo %TEMP%') }).stdout.chomp.gsub('\\', '/')
87
+ (@connection.shell(:powershell) { |e| e.run('$env:TEMP') }).stdout.chomp.gsub('\\', '/')
88
88
  end
89
89
  end
90
90
 
@@ -107,8 +107,8 @@ module WinRM
107
107
  # @yieldparam [String] Target path on the winrm endpoint
108
108
  # @return [Fixnum] The total number of bytes copied
109
109
  def upload(local_path, remote_path, &block)
110
- @service.create_executor do |executor|
111
- file_transporter ||= WinRM::FS::Core::FileTransporter.new(executor)
110
+ @connection.shell(:powershell) do |shell|
111
+ file_transporter ||= WinRM::FS::Core::FileTransporter.new(shell)
112
112
  file_transporter.upload(local_path, remote_path, &block)[0]
113
113
  end
114
114
  end