vagrant-unbundled 2.2.6.1 → 2.2.6.2
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/Gemfile.lock +11 -9
- data/vagrant.gemspec +1 -1
- data/vendor/bundle/ruby/2.6.0/bundler/gems/vagrant-spec-abfc34474d12/vagrant-spec.gemspec +1 -1
- data/vendor/bundle/ruby/2.6.0/gems/erubi-1.9.0/CHANGELOG +79 -0
- data/vendor/bundle/ruby/2.6.0/gems/erubi-1.9.0/MIT-LICENSE +21 -0
- data/vendor/bundle/ruby/2.6.0/gems/erubi-1.9.0/README.rdoc +109 -0
- data/vendor/bundle/ruby/2.6.0/gems/erubi-1.9.0/Rakefile +78 -0
- data/vendor/bundle/ruby/2.6.0/gems/erubi-1.9.0/lib/erubi/capture_end.rb +52 -0
- data/vendor/bundle/ruby/2.6.0/gems/erubi-1.9.0/lib/erubi.rb +211 -0
- data/vendor/bundle/ruby/2.6.0/gems/erubi-1.9.0/test/test.rb +780 -0
- data/vendor/bundle/ruby/2.6.0/gems/rubyzip-2.0.0/README.md +354 -0
- data/vendor/bundle/ruby/2.6.0/gems/rubyzip-2.0.0/Rakefile +18 -0
- data/vendor/bundle/ruby/2.6.0/gems/rubyzip-2.0.0/TODO +15 -0
- data/vendor/bundle/ruby/2.6.0/gems/rubyzip-2.0.0/lib/zip/central_directory.rb +208 -0
- data/vendor/bundle/ruby/2.6.0/gems/rubyzip-2.0.0/lib/zip/compressor.rb +9 -0
- data/vendor/bundle/ruby/2.6.0/gems/rubyzip-2.0.0/lib/zip/constants.rb +63 -0
- data/vendor/bundle/ruby/2.6.0/gems/rubyzip-2.0.0/lib/zip/crypto/encryption.rb +11 -0
- data/vendor/bundle/ruby/2.6.0/gems/rubyzip-2.0.0/lib/zip/crypto/null_encryption.rb +43 -0
- data/vendor/bundle/ruby/2.6.0/gems/rubyzip-2.0.0/lib/zip/crypto/traditional_encryption.rb +99 -0
- data/vendor/bundle/ruby/2.6.0/gems/rubyzip-2.0.0/lib/zip/decompressor.rb +13 -0
- data/vendor/bundle/ruby/2.6.0/gems/rubyzip-2.0.0/lib/zip/deflater.rb +34 -0
- data/vendor/bundle/ruby/2.6.0/gems/rubyzip-2.0.0/lib/zip/dos_time.rb +48 -0
- data/vendor/bundle/ruby/2.6.0/gems/rubyzip-2.0.0/lib/zip/entry.rb +700 -0
- data/vendor/bundle/ruby/2.6.0/gems/rubyzip-2.0.0/lib/zip/entry_set.rb +86 -0
- data/vendor/bundle/ruby/2.6.0/gems/rubyzip-2.0.0/lib/zip/errors.rb +18 -0
- data/vendor/bundle/ruby/2.6.0/gems/rubyzip-2.0.0/lib/zip/extra_field/generic.rb +43 -0
- data/vendor/bundle/ruby/2.6.0/gems/rubyzip-2.0.0/lib/zip/extra_field/ntfs.rb +90 -0
- data/vendor/bundle/ruby/2.6.0/gems/rubyzip-2.0.0/lib/zip/extra_field/old_unix.rb +44 -0
- data/vendor/bundle/ruby/2.6.0/gems/rubyzip-2.0.0/lib/zip/extra_field/universal_time.rb +47 -0
- data/vendor/bundle/ruby/2.6.0/gems/rubyzip-2.0.0/lib/zip/extra_field/unix.rb +37 -0
- data/vendor/bundle/ruby/2.6.0/gems/rubyzip-2.0.0/lib/zip/extra_field/zip64.rb +68 -0
- data/vendor/bundle/ruby/2.6.0/gems/rubyzip-2.0.0/lib/zip/extra_field/zip64_placeholder.rb +15 -0
- data/vendor/bundle/ruby/2.6.0/gems/rubyzip-2.0.0/lib/zip/extra_field.rb +101 -0
- data/vendor/bundle/ruby/2.6.0/gems/rubyzip-2.0.0/lib/zip/file.rb +443 -0
- data/vendor/bundle/ruby/2.6.0/gems/rubyzip-2.0.0/lib/zip/filesystem.rb +627 -0
- data/vendor/bundle/ruby/2.6.0/gems/rubyzip-2.0.0/lib/zip/inflater.rb +66 -0
- data/vendor/bundle/ruby/2.6.0/gems/rubyzip-2.0.0/lib/zip/input_stream.rb +173 -0
- data/vendor/bundle/ruby/2.6.0/gems/rubyzip-2.0.0/lib/zip/ioextras/abstract_input_stream.rb +111 -0
- data/vendor/bundle/ruby/2.6.0/gems/rubyzip-2.0.0/lib/zip/ioextras/abstract_output_stream.rb +43 -0
- data/vendor/bundle/ruby/2.6.0/gems/rubyzip-2.0.0/lib/zip/ioextras.rb +36 -0
- data/vendor/bundle/ruby/2.6.0/gems/rubyzip-2.0.0/lib/zip/null_compressor.rb +15 -0
- data/vendor/bundle/ruby/2.6.0/gems/rubyzip-2.0.0/lib/zip/null_decompressor.rb +27 -0
- data/vendor/bundle/ruby/2.6.0/gems/rubyzip-2.0.0/lib/zip/null_input_stream.rb +10 -0
- data/vendor/bundle/ruby/2.6.0/gems/rubyzip-2.0.0/lib/zip/output_stream.rb +189 -0
- data/vendor/bundle/ruby/2.6.0/gems/rubyzip-2.0.0/lib/zip/pass_thru_compressor.rb +23 -0
- data/vendor/bundle/ruby/2.6.0/gems/rubyzip-2.0.0/lib/zip/pass_thru_decompressor.rb +40 -0
- data/vendor/bundle/ruby/2.6.0/gems/rubyzip-2.0.0/lib/zip/streamable_directory.rb +15 -0
- data/vendor/bundle/ruby/2.6.0/gems/rubyzip-2.0.0/lib/zip/streamable_stream.rb +56 -0
- data/vendor/bundle/ruby/2.6.0/gems/rubyzip-2.0.0/lib/zip/version.rb +3 -0
- data/vendor/bundle/ruby/2.6.0/gems/rubyzip-2.0.0/lib/zip.rb +71 -0
- data/vendor/bundle/ruby/2.6.0/gems/rubyzip-2.0.0/samples/example.rb +81 -0
- data/vendor/bundle/ruby/2.6.0/gems/rubyzip-2.0.0/samples/example_filesystem.rb +31 -0
- data/vendor/bundle/ruby/2.6.0/gems/rubyzip-2.0.0/samples/example_recursive.rb +54 -0
- data/vendor/bundle/ruby/2.6.0/gems/rubyzip-2.0.0/samples/gtk_ruby_zip.rb +84 -0
- data/vendor/bundle/ruby/2.6.0/gems/rubyzip-2.0.0/samples/qtzip.rb +92 -0
- data/vendor/bundle/ruby/2.6.0/gems/rubyzip-2.0.0/samples/write_simple.rb +12 -0
- data/vendor/bundle/ruby/2.6.0/gems/rubyzip-2.0.0/samples/zipfind.rb +66 -0
- data/vendor/bundle/ruby/2.6.0/gems/winrm-2.3.3/LICENSE +202 -0
- data/vendor/bundle/ruby/2.6.0/gems/winrm-2.3.3/README.md +276 -0
- data/vendor/bundle/ruby/2.6.0/gems/winrm-2.3.3/bin/rwinrm +90 -0
- data/vendor/bundle/ruby/2.6.0/gems/winrm-2.3.3/lib/winrm/connection.rb +84 -0
- data/vendor/bundle/ruby/2.6.0/gems/winrm-2.3.3/lib/winrm/connection_opts.rb +90 -0
- data/vendor/bundle/ruby/2.6.0/gems/winrm-2.3.3/lib/winrm/exceptions.rb +88 -0
- data/vendor/bundle/ruby/2.6.0/gems/winrm-2.3.3/lib/winrm/http/response_handler.rb +127 -0
- data/vendor/bundle/ruby/2.6.0/gems/winrm-2.3.3/lib/winrm/http/transport.rb +462 -0
- data/vendor/bundle/ruby/2.6.0/gems/winrm-2.3.3/lib/winrm/http/transport_factory.rb +64 -0
- data/vendor/bundle/ruby/2.6.0/gems/winrm-2.3.3/lib/winrm/output.rb +58 -0
- data/vendor/bundle/ruby/2.6.0/gems/winrm-2.3.3/lib/winrm/psrp/create_pipeline.xml.erb +167 -0
- data/vendor/bundle/ruby/2.6.0/gems/winrm-2.3.3/lib/winrm/psrp/fragment.rb +68 -0
- data/vendor/bundle/ruby/2.6.0/gems/winrm-2.3.3/lib/winrm/psrp/init_runspace_pool.xml.erb +224 -0
- data/vendor/bundle/ruby/2.6.0/gems/winrm-2.3.3/lib/winrm/psrp/message.rb +128 -0
- data/vendor/bundle/ruby/2.6.0/gems/winrm-2.3.3/lib/winrm/psrp/message_data/base.rb +47 -0
- data/vendor/bundle/ruby/2.6.0/gems/winrm-2.3.3/lib/winrm/psrp/message_data/error_record.rb +66 -0
- data/vendor/bundle/ruby/2.6.0/gems/winrm-2.3.3/lib/winrm/psrp/message_data/pipeline_host_call.rb +30 -0
- data/vendor/bundle/ruby/2.6.0/gems/winrm-2.3.3/lib/winrm/psrp/message_data/pipeline_output.rb +48 -0
- data/vendor/bundle/ruby/2.6.0/gems/winrm-2.3.3/lib/winrm/psrp/message_data/pipeline_state.rb +38 -0
- data/vendor/bundle/ruby/2.6.0/gems/winrm-2.3.3/lib/winrm/psrp/message_data/runspacepool_host_call.rb +30 -0
- data/vendor/bundle/ruby/2.6.0/gems/winrm-2.3.3/lib/winrm/psrp/message_data/runspacepool_state.rb +37 -0
- data/vendor/bundle/ruby/2.6.0/gems/winrm-2.3.3/lib/winrm/psrp/message_data/session_capability.rb +34 -0
- data/vendor/bundle/ruby/2.6.0/gems/winrm-2.3.3/lib/winrm/psrp/message_data.rb +40 -0
- data/vendor/bundle/ruby/2.6.0/gems/winrm-2.3.3/lib/winrm/psrp/message_defragmenter.rb +62 -0
- data/vendor/bundle/ruby/2.6.0/gems/winrm-2.3.3/lib/winrm/psrp/message_factory.rb +86 -0
- data/vendor/bundle/ruby/2.6.0/gems/winrm-2.3.3/lib/winrm/psrp/message_fragmenter.rb +58 -0
- data/vendor/bundle/ruby/2.6.0/gems/winrm-2.3.3/lib/winrm/psrp/powershell_output_decoder.rb +142 -0
- data/vendor/bundle/ruby/2.6.0/gems/winrm-2.3.3/lib/winrm/psrp/receive_response_reader.rb +95 -0
- data/vendor/bundle/ruby/2.6.0/gems/winrm-2.3.3/lib/winrm/psrp/session_capability.xml.erb +7 -0
- data/vendor/bundle/ruby/2.6.0/gems/winrm-2.3.3/lib/winrm/psrp/uuid.rb +39 -0
- data/vendor/bundle/ruby/2.6.0/gems/winrm-2.3.3/lib/winrm/shells/base.rb +187 -0
- data/vendor/bundle/ruby/2.6.0/gems/winrm-2.3.3/lib/winrm/shells/cmd.rb +63 -0
- data/vendor/bundle/ruby/2.6.0/gems/winrm-2.3.3/lib/winrm/shells/power_shell.rb +206 -0
- data/vendor/bundle/ruby/2.6.0/gems/winrm-2.3.3/lib/winrm/shells/retryable.rb +44 -0
- data/vendor/bundle/ruby/2.6.0/gems/winrm-2.3.3/lib/winrm/shells/shell_factory.rb +56 -0
- data/vendor/bundle/ruby/2.6.0/gems/winrm-2.3.3/lib/winrm/version.rb +5 -0
- data/vendor/bundle/ruby/2.6.0/gems/winrm-2.3.3/lib/winrm/wsmv/base.rb +57 -0
- data/vendor/bundle/ruby/2.6.0/gems/winrm-2.3.3/lib/winrm/wsmv/cleanup_command.rb +60 -0
- data/vendor/bundle/ruby/2.6.0/gems/winrm-2.3.3/lib/winrm/wsmv/close_shell.rb +49 -0
- data/vendor/bundle/ruby/2.6.0/gems/winrm-2.3.3/lib/winrm/wsmv/command.rb +100 -0
- data/vendor/bundle/ruby/2.6.0/gems/winrm-2.3.3/lib/winrm/wsmv/command_output.rb +75 -0
- data/vendor/bundle/ruby/2.6.0/gems/winrm-2.3.3/lib/winrm/wsmv/command_output_decoder.rb +54 -0
- data/vendor/bundle/ruby/2.6.0/gems/winrm-2.3.3/lib/winrm/wsmv/configuration.rb +44 -0
- data/vendor/bundle/ruby/2.6.0/gems/winrm-2.3.3/lib/winrm/wsmv/create_pipeline.rb +64 -0
- data/vendor/bundle/ruby/2.6.0/gems/winrm-2.3.3/lib/winrm/wsmv/create_shell.rb +115 -0
- data/vendor/bundle/ruby/2.6.0/gems/winrm-2.3.3/lib/winrm/wsmv/header.rb +213 -0
- data/vendor/bundle/ruby/2.6.0/gems/winrm-2.3.3/lib/winrm/wsmv/init_runspace_pool.rb +96 -0
- data/vendor/bundle/ruby/2.6.0/gems/winrm-2.3.3/lib/winrm/wsmv/iso8601_duration.rb +58 -0
- data/vendor/bundle/ruby/2.6.0/gems/winrm-2.3.3/lib/winrm/wsmv/keep_alive.rb +66 -0
- data/vendor/bundle/ruby/2.6.0/gems/winrm-2.3.3/lib/winrm/wsmv/receive_response_reader.rb +128 -0
- data/vendor/bundle/ruby/2.6.0/gems/winrm-2.3.3/lib/winrm/wsmv/send_data.rb +66 -0
- data/vendor/bundle/ruby/2.6.0/gems/winrm-2.3.3/lib/winrm/wsmv/soap.rb +49 -0
- data/vendor/bundle/ruby/2.6.0/gems/winrm-2.3.3/lib/winrm/wsmv/wql_pull.rb +54 -0
- data/vendor/bundle/ruby/2.6.0/gems/winrm-2.3.3/lib/winrm/wsmv/wql_query.rb +98 -0
- data/vendor/bundle/ruby/2.6.0/gems/winrm-2.3.3/lib/winrm/wsmv/write_stdin.rb +86 -0
- data/vendor/bundle/ruby/2.6.0/gems/winrm-2.3.3/lib/winrm.rb +37 -0
- data/vendor/bundle/ruby/2.6.0/gems/winrm-elevated-1.1.2/LICENSE +202 -0
- data/vendor/bundle/ruby/2.6.0/gems/winrm-elevated-1.1.2/README.md +99 -0
- data/vendor/bundle/ruby/2.6.0/gems/winrm-elevated-1.1.2/lib/winrm/shells/elevated.rb +108 -0
- data/vendor/bundle/ruby/2.6.0/gems/winrm-elevated-1.1.2/lib/winrm-elevated/scripts/elevated_shell.ps1 +116 -0
- data/vendor/bundle/ruby/2.6.0/gems/winrm-elevated-1.1.2/lib/winrm-elevated.rb +17 -0
- data/vendor/bundle/ruby/2.6.0/gems/winrm-fs-1.3.4/LICENSE +202 -0
- data/vendor/bundle/ruby/2.6.0/gems/winrm-fs-1.3.4/README.md +82 -0
- data/vendor/bundle/ruby/2.6.0/gems/winrm-fs-1.3.4/bin/rwinrmcp +87 -0
- data/vendor/bundle/ruby/2.6.0/gems/winrm-fs-1.3.4/lib/winrm-fs/core/file_transporter.rb +569 -0
- data/vendor/bundle/ruby/2.6.0/gems/winrm-fs-1.3.4/lib/winrm-fs/core/tmp_zip.rb +178 -0
- data/vendor/bundle/ruby/2.6.0/gems/winrm-fs-1.3.4/lib/winrm-fs/exceptions.rb +29 -0
- data/vendor/bundle/ruby/2.6.0/gems/winrm-fs-1.3.4/lib/winrm-fs/file_manager.rb +158 -0
- data/vendor/bundle/ruby/2.6.0/gems/winrm-fs-1.3.4/lib/winrm-fs/scripts/check_files.ps1.erb +49 -0
- data/vendor/bundle/ruby/2.6.0/gems/winrm-fs-1.3.4/lib/winrm-fs/scripts/checksum.ps1.erb +13 -0
- data/vendor/bundle/ruby/2.6.0/gems/winrm-fs-1.3.4/lib/winrm-fs/scripts/create_dir.ps1.erb +6 -0
- data/vendor/bundle/ruby/2.6.0/gems/winrm-fs-1.3.4/lib/winrm-fs/scripts/delete.ps1.erb +6 -0
- data/vendor/bundle/ruby/2.6.0/gems/winrm-fs-1.3.4/lib/winrm-fs/scripts/download.ps1.erb +17 -0
- data/vendor/bundle/ruby/2.6.0/gems/winrm-fs-1.3.4/lib/winrm-fs/scripts/exists.ps1.erb +10 -0
- data/vendor/bundle/ruby/2.6.0/gems/winrm-fs-1.3.4/lib/winrm-fs/scripts/extract_files.ps1.erb +52 -0
- data/vendor/bundle/ruby/2.6.0/gems/winrm-fs-1.3.4/lib/winrm-fs/scripts/scripts.rb +47 -0
- data/vendor/bundle/ruby/2.6.0/gems/winrm-fs-1.3.4/lib/winrm-fs.rb +29 -0
- data/vendor/bundle/ruby/2.6.0/specifications/erubi-1.9.0.gemspec +38 -0
- data/vendor/bundle/ruby/2.6.0/specifications/rubyzip-2.0.0.gemspec +45 -0
- data/vendor/bundle/ruby/2.6.0/specifications/winrm-2.3.3.gemspec +73 -0
- data/vendor/bundle/ruby/2.6.0/specifications/winrm-elevated-1.1.2.gemspec +50 -0
- data/vendor/bundle/ruby/2.6.0/specifications/winrm-fs-1.3.4.gemspec +58 -0
- data/version.txt +1 -1
- metadata +142 -6
@@ -0,0 +1,569 @@
|
|
1
|
+
# frozen_string_literal: false
|
2
|
+
|
3
|
+
#
|
4
|
+
# Author:: Fletcher (<fnichol@nichol.ca>)
|
5
|
+
#
|
6
|
+
# Copyright (C) 2015, Fletcher Nichol
|
7
|
+
#
|
8
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
9
|
+
# you may not use this file except in compliance with the License.
|
10
|
+
# You may obtain a copy of the License at
|
11
|
+
#
|
12
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
13
|
+
#
|
14
|
+
# Unless required by applicable law or agreed to in writing, software
|
15
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
16
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
17
|
+
# See the License for the specific language governing permissions and
|
18
|
+
# limitations under the License.
|
19
|
+
|
20
|
+
require 'benchmark'
|
21
|
+
require 'csv'
|
22
|
+
require 'digest'
|
23
|
+
require 'securerandom'
|
24
|
+
require 'stringio'
|
25
|
+
|
26
|
+
require 'winrm/exceptions'
|
27
|
+
require 'winrm-fs/core/tmp_zip'
|
28
|
+
|
29
|
+
module WinRM
|
30
|
+
module FS
|
31
|
+
module Core
|
32
|
+
# Wrapped exception for any internally raised WinRM-related errors.
|
33
|
+
#
|
34
|
+
# @author Fletcher Nichol <fnichol@nichol.ca>
|
35
|
+
class FileTransporterFailed < ::WinRM::WinRMError; end
|
36
|
+
# rubocop:disable Metrics/MethodLength, Metrics/AbcSize, Metrics/ClassLength
|
37
|
+
|
38
|
+
# Exception for the case where upload source contains more than one
|
39
|
+
# StringIO object, or a combination of file/directory paths and StringIO object
|
40
|
+
class UploadSourceError < StandardError
|
41
|
+
def initialize(msg = 'Only a single StringIO object may be uploaded.')
|
42
|
+
super
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
# Object which can upload one or more files or directories to a remote
|
47
|
+
# host over WinRM using PowerShell scripts and CMD commands. Note that
|
48
|
+
# this form of file transfer is *not* ideal and extremely costly on both
|
49
|
+
# the local and remote sides. Great pains are made to minimize round
|
50
|
+
# trips to the remote host and to minimize the number of PowerShell
|
51
|
+
# sessions being invoked which can be 2 orders of magnitude more
|
52
|
+
# expensive than vanilla CMD commands.
|
53
|
+
#
|
54
|
+
# This object is supported by a `PowerShell` instance as it
|
55
|
+
# depends on the `#run` API contract.
|
56
|
+
#
|
57
|
+
# An optional logger can be supplied, assuming it can respond to the
|
58
|
+
# `#debug` and `#debug?` messages.
|
59
|
+
#
|
60
|
+
# @author Fletcher Nichol <fnichol@nichol.ca>
|
61
|
+
# @author Matt Wrock <matt@mattwrock.com>
|
62
|
+
class FileTransporter
|
63
|
+
# Creates a FileTransporter given a PowerShell object.
|
64
|
+
#
|
65
|
+
# @param shell [PowerShell] a winrm PowerShell object
|
66
|
+
def initialize(shell, opts = {})
|
67
|
+
@shell = shell
|
68
|
+
@logger = shell.logger
|
69
|
+
@id_generator = opts.fetch(:id_generator) { -> { SecureRandom.uuid } }
|
70
|
+
Zip.unicode_names = true
|
71
|
+
end
|
72
|
+
|
73
|
+
# Uploads a collection of files and/or directories to the remote host.
|
74
|
+
#
|
75
|
+
# **TODO Notes:**
|
76
|
+
# * options could specify zip mode, zip options, etc.
|
77
|
+
# * maybe option to set tmpfile base dir to override $env:PATH?
|
78
|
+
# * progress yields block like net-scp progress
|
79
|
+
# * final API: def upload(locals, remote, _options = {}, &_progress)
|
80
|
+
#
|
81
|
+
# @param locals [Array<String>,String,StringIO] one or more
|
82
|
+
# local file or directory paths, StringIO objects also accepted
|
83
|
+
# @param remote [String] the base destination path on the remote host
|
84
|
+
# @return [Hash] report hash, keyed by the local SHA1 digest
|
85
|
+
def upload(locals, remote)
|
86
|
+
files = nil
|
87
|
+
report = nil
|
88
|
+
remote = remote.to_s
|
89
|
+
elapsed1 = Benchmark.measure do
|
90
|
+
files = make_files_hash([locals].flatten, remote)
|
91
|
+
report = check_files(files)
|
92
|
+
merge_with_report!(files, report)
|
93
|
+
reconcile_destinations!(files)
|
94
|
+
end
|
95
|
+
total_size = total_base64_transfer_size(files)
|
96
|
+
|
97
|
+
elapsed2 = Benchmark.measure do
|
98
|
+
report = stream_upload_files(files) do |local_path, xfered|
|
99
|
+
yield xfered, total_size, local_path, remote if block_given?
|
100
|
+
end
|
101
|
+
merge_with_report!(files, report)
|
102
|
+
end
|
103
|
+
|
104
|
+
elapsed3 = Benchmark.measure do
|
105
|
+
report = extract_files(files)
|
106
|
+
merge_with_report!(files, report)
|
107
|
+
cleanup(files)
|
108
|
+
end
|
109
|
+
|
110
|
+
logger.debug(
|
111
|
+
"Uploaded #{files.keys.size} items " \
|
112
|
+
"dirty_check: #{duration(elapsed1.real)} " \
|
113
|
+
"stream_files: #{duration(elapsed2.real)} " \
|
114
|
+
"extract: #{duration(elapsed3.real)} " \
|
115
|
+
)
|
116
|
+
|
117
|
+
[total_size, files]
|
118
|
+
end
|
119
|
+
|
120
|
+
private
|
121
|
+
|
122
|
+
# @return [String] the Array pack template for Base64 encoding a stream
|
123
|
+
# of data
|
124
|
+
# @api private
|
125
|
+
BASE64_PACK = 'm0'.freeze
|
126
|
+
|
127
|
+
# @return [String] the directory where temporary upload artifacts are
|
128
|
+
# persisted
|
129
|
+
# @api private
|
130
|
+
TEMP_UPLOAD_DIRECTORY = '$env:TEMP\\winrm-upload'.freeze
|
131
|
+
|
132
|
+
# @return [#debug,#debug?] the logger
|
133
|
+
# @api private
|
134
|
+
attr_reader :logger
|
135
|
+
|
136
|
+
# @return [Winrm::Shells::Powershell] a WinRM Powershell shell
|
137
|
+
# @api private
|
138
|
+
attr_reader :shell
|
139
|
+
|
140
|
+
# @return [Integer] the maximum number of bytes to send per request
|
141
|
+
# when streaming a file. This is optimized to send as much data
|
142
|
+
# as allowed in a single PSRP fragment
|
143
|
+
# @api private
|
144
|
+
def max_encoded_write
|
145
|
+
@max_encoded_write ||= begin
|
146
|
+
empty_command = WinRM::PSRP::MessageFactory.create_pipeline_message(
|
147
|
+
'00000000-0000-0000-0000-000000000000',
|
148
|
+
'00000000-0000-0000-0000-000000000000',
|
149
|
+
stream_command('')
|
150
|
+
)
|
151
|
+
shell.max_fragment_blob_size - empty_command.bytes.length
|
152
|
+
end
|
153
|
+
end
|
154
|
+
|
155
|
+
# Examines the files and corrects the file destination if it is
|
156
|
+
# targeting an existing folder. In this case, the destination path
|
157
|
+
# will have the base name of the source file appended. This only
|
158
|
+
# applies to file uploads and not to folder uploads.
|
159
|
+
#
|
160
|
+
# @param files [Hash] files hash, keyed by the local SHA1 digest
|
161
|
+
# @return [Hash] a report hash, keyed by the local SHA1 digest
|
162
|
+
# @api private
|
163
|
+
def reconcile_destinations!(files)
|
164
|
+
files.each do |_, data|
|
165
|
+
data['dst'] = File.join(data['dst'], File.basename(data['src'])) if data['target_is_folder'] == 'True'
|
166
|
+
end
|
167
|
+
end
|
168
|
+
|
169
|
+
# Adds an entry to a files Hash (keyed by local SHA1 digest) for a
|
170
|
+
# directory. When a directory is added, a temporary Zip file is created
|
171
|
+
# containing the contents of the directory and any file-related data
|
172
|
+
# such as SHA1 digest, size, etc. will be referring to the Zip file.
|
173
|
+
#
|
174
|
+
# @param hash [Hash] hash to be mutated
|
175
|
+
# @param dir [String] directory path to be Zipped and added
|
176
|
+
# @param remote [String] path to destination on remote host
|
177
|
+
# @api private
|
178
|
+
def add_directory_hash!(hash, dir, remote)
|
179
|
+
logger.debug "creating hash for directory #{remote}"
|
180
|
+
zip_io = TmpZip.new(dir, logger)
|
181
|
+
zip_sha1 = sha1sum(zip_io.path)
|
182
|
+
|
183
|
+
hash[zip_sha1] = {
|
184
|
+
'src' => dir,
|
185
|
+
'src_zip' => zip_io.path.to_s,
|
186
|
+
'zip_io' => zip_io,
|
187
|
+
'tmpzip' => "#{TEMP_UPLOAD_DIRECTORY}\\tmpzip-#{zip_sha1}.zip",
|
188
|
+
'dst' => "#{remote}\\#{File.basename(dir)}",
|
189
|
+
'size' => File.size(zip_io.path)
|
190
|
+
}
|
191
|
+
end
|
192
|
+
|
193
|
+
# Adds an entry to a files Hash (keyed by local SHA1 digest) for a file.
|
194
|
+
#
|
195
|
+
# @param hash [Hash] hash to be mutated
|
196
|
+
# @param local [String, StringIO] file path or StringIO object
|
197
|
+
# @param remote [String] path to destination on remote host
|
198
|
+
# @api private
|
199
|
+
def add_file_hash!(hash, local, remote)
|
200
|
+
logger.debug "creating hash for file #{remote}"
|
201
|
+
hash[sha1sum(local)] = {
|
202
|
+
'src' => local,
|
203
|
+
'dst' => remote,
|
204
|
+
'size' => local.is_a?(StringIO) ? local.size : File.size(local)
|
205
|
+
}
|
206
|
+
end
|
207
|
+
|
208
|
+
# Runs the check_files PowerShell script against a collection of
|
209
|
+
# destination path/SHA1 checksum pairs. The PowerShell script returns
|
210
|
+
# its results as a CSV-formatted report which is converted into a Ruby
|
211
|
+
# Hash.
|
212
|
+
#
|
213
|
+
# @param files [Hash] files hash, keyed by the local SHA1 digest
|
214
|
+
# @return [Hash] a report hash, keyed by the local SHA1 digest
|
215
|
+
# @api private
|
216
|
+
def check_files(files)
|
217
|
+
logger.debug 'Running check_files.ps1'
|
218
|
+
hash_file = check_files_ps_hash(files)
|
219
|
+
script = WinRM::FS::Scripts.render('check_files', hash_file: hash_file)
|
220
|
+
parse_response(shell.run(script))
|
221
|
+
end
|
222
|
+
|
223
|
+
# Constructs a collection of destination path/SHA1 checksum pairs as a
|
224
|
+
# String representation of the contents of a PowerShell Hash Table.
|
225
|
+
#
|
226
|
+
# @param files [Hash] files hash, keyed by the local SHA1 digest
|
227
|
+
# @return [String] the inner contents of a PowerShell Hash Table
|
228
|
+
# @api private
|
229
|
+
def check_files_ps_hash(files)
|
230
|
+
hash = files.map do |sha1, data|
|
231
|
+
[
|
232
|
+
sha1,
|
233
|
+
{
|
234
|
+
'target' => data.fetch('tmpzip', data['dst']),
|
235
|
+
'src_basename' => data['src'].is_a?(StringIO) ? data['dst'] : File.basename(data['src']),
|
236
|
+
'dst' => data['dst']
|
237
|
+
}
|
238
|
+
]
|
239
|
+
end
|
240
|
+
ps_hash(Hash[hash])
|
241
|
+
end
|
242
|
+
|
243
|
+
# Performs any final cleanup on the report Hash and removes any
|
244
|
+
# temporary files/resources used in the upload task.
|
245
|
+
#
|
246
|
+
# @param files [Hash] a files hash
|
247
|
+
# @api private
|
248
|
+
def cleanup(files)
|
249
|
+
files.select { |_, data| data.key?('zip_io') }.each do |sha1, data|
|
250
|
+
data.fetch('zip_io').unlink
|
251
|
+
files.fetch(sha1).delete('zip_io')
|
252
|
+
logger.debug "Cleaned up src_zip #{data['src_zip']}"
|
253
|
+
end
|
254
|
+
end
|
255
|
+
|
256
|
+
# Runs the extract_files PowerShell script against a collection of
|
257
|
+
# temporary file/destination path pairs. The PowerShell script returns
|
258
|
+
# its results as a CSV-formatted report which is converted into a Ruby
|
259
|
+
# Hash. The script will not be invoked if there are no zip files
|
260
|
+
# present in the incoming files Hash.
|
261
|
+
#
|
262
|
+
# @param files [Hash] files hash, keyed by the local SHA1 digest
|
263
|
+
# @return [Hash] a report hash, keyed by the local SHA1 digest
|
264
|
+
# @api private
|
265
|
+
def extract_files(files)
|
266
|
+
extracted_files = extract_files_ps_hash(files)
|
267
|
+
|
268
|
+
if extracted_files == ps_hash({})
|
269
|
+
logger.debug 'No remote files to extract, skipping'
|
270
|
+
{}
|
271
|
+
else
|
272
|
+
logger.debug 'Running extract_files.ps1'
|
273
|
+
script = WinRM::FS::Scripts.render('extract_files', hash_file: extracted_files)
|
274
|
+
|
275
|
+
parse_response(shell.run(script))
|
276
|
+
end
|
277
|
+
end
|
278
|
+
|
279
|
+
# Constructs a collection of temporary file/destination path pairs for
|
280
|
+
# all zipped folders as a String representation of the contents of a
|
281
|
+
# PowerShell Hash Table.
|
282
|
+
#
|
283
|
+
# @param files [Hash] files hash, keyed by the local SHA1 digest
|
284
|
+
# @return [String] the inner contents of a PowerShell Hash Table
|
285
|
+
# @api private
|
286
|
+
def extract_files_ps_hash(files)
|
287
|
+
file_data = files.select { |_, data| data.key?('tmpzip') }
|
288
|
+
|
289
|
+
result = file_data.map do |sha1, data|
|
290
|
+
val = { 'dst' => data['dst'] }
|
291
|
+
val['tmpzip'] = data['tmpzip'] if data['tmpzip']
|
292
|
+
|
293
|
+
[sha1, val]
|
294
|
+
end
|
295
|
+
|
296
|
+
ps_hash(Hash[result])
|
297
|
+
end
|
298
|
+
|
299
|
+
# Returns a formatted string representing a duration in seconds.
|
300
|
+
#
|
301
|
+
# @param total [Integer] the total number of seconds
|
302
|
+
# @return [String] a formatted string of the form (XmYY.00s)
|
303
|
+
def duration(total)
|
304
|
+
total = 0 if total.nil?
|
305
|
+
minutes = (total / 60).to_i
|
306
|
+
seconds = (total - (minutes * 60))
|
307
|
+
format('(%dm%.2fs)', minutes, seconds)
|
308
|
+
end
|
309
|
+
|
310
|
+
# Contructs a Hash of files or directories, keyed by the local SHA1
|
311
|
+
# digest. Each file entry has a source and destination set, at a
|
312
|
+
# minimum.
|
313
|
+
#
|
314
|
+
# @param locals [Array<String,StringIO>] a collection of local files,
|
315
|
+
# directories or StringIO objects
|
316
|
+
# @param remote [String] the base destination path on the remote host
|
317
|
+
# @return [Hash] files hash, keyed by the local SHA1 digest
|
318
|
+
# @api private
|
319
|
+
def make_files_hash(locals, remote)
|
320
|
+
hash = {}
|
321
|
+
check_locals_array(locals)
|
322
|
+
locals.each do |local|
|
323
|
+
if local.is_a?(StringIO)
|
324
|
+
add_file_hash!(hash, local, remote)
|
325
|
+
else
|
326
|
+
local = local.to_s
|
327
|
+
expanded = File.expand_path(local)
|
328
|
+
expanded += local[-1] if local.end_with?('/', '\\')
|
329
|
+
if File.file?(expanded)
|
330
|
+
add_file_hash!(hash, expanded, remote)
|
331
|
+
elsif File.directory?(expanded)
|
332
|
+
add_directory_hash!(hash, expanded, remote)
|
333
|
+
else
|
334
|
+
raise Errno::ENOENT, "No such file or directory #{expanded}"
|
335
|
+
end
|
336
|
+
end
|
337
|
+
end
|
338
|
+
hash
|
339
|
+
end
|
340
|
+
|
341
|
+
# Ensure that only a single StringIO object is uploaded at a time
|
342
|
+
# This is necessary because the contents of the buffer will be written
|
343
|
+
# to the destination.
|
344
|
+
# @param locals [Array<String,StringIO>] a collection of local files,
|
345
|
+
# directories or StringIO objects
|
346
|
+
# @api private
|
347
|
+
def check_locals_array(locals)
|
348
|
+
string_io = false
|
349
|
+
path = false
|
350
|
+
locals.each do |local|
|
351
|
+
raise UploadSourceError if string_io
|
352
|
+
|
353
|
+
if local.is_a?(StringIO)
|
354
|
+
string_io = true
|
355
|
+
else
|
356
|
+
path = true
|
357
|
+
end
|
358
|
+
raise UploadSourceError if string_io && path
|
359
|
+
end
|
360
|
+
end
|
361
|
+
|
362
|
+
# @return [String] the SHA1 digest of a local file or StringIO
|
363
|
+
# @api private
|
364
|
+
def sha1sum(local)
|
365
|
+
if local.is_a?(StringIO)
|
366
|
+
Digest::SHA1.hexdigest(local.string)
|
367
|
+
else
|
368
|
+
Digest::SHA1.file(local).hexdigest
|
369
|
+
end
|
370
|
+
end
|
371
|
+
|
372
|
+
# Destructively merges a report Hash into an existing files Hash.
|
373
|
+
# **Note:** this method mutates the files Hash.
|
374
|
+
#
|
375
|
+
# @param files [Hash] files hash, keyed by the local SHA1 digest
|
376
|
+
# @param report [Hash] report hash, keyed by the local SHA1 digest
|
377
|
+
# @api private
|
378
|
+
def merge_with_report!(files, report)
|
379
|
+
files.merge!(report) { |_, oldval, newval| oldval.merge(newval) }
|
380
|
+
end
|
381
|
+
|
382
|
+
# @param depth [Integer] number of padding characters (default: `0`)
|
383
|
+
# @return [String] a whitespace padded string of the given length
|
384
|
+
# @api private
|
385
|
+
def pad(depth = 0)
|
386
|
+
' ' * depth
|
387
|
+
end
|
388
|
+
|
389
|
+
# Parses response of a PowerShell script or CMD command which contains
|
390
|
+
# a CSV-formatted document in the standard output stream.
|
391
|
+
#
|
392
|
+
# @param output [WinRM::Output] output object with stdout, stderr, and
|
393
|
+
# exit code
|
394
|
+
# @return [Hash] report hash, keyed by the local SHA1 digest
|
395
|
+
# @api private
|
396
|
+
def parse_response(output)
|
397
|
+
exitcode = output.exitcode
|
398
|
+
stderr = output.stderr
|
399
|
+
|
400
|
+
if exitcode != 0
|
401
|
+
raise FileTransporterFailed, "[#{self.class}] Upload failed " \
|
402
|
+
"(exitcode: #{exitcode})\n#{stderr}"
|
403
|
+
elsif stderr != '\r\n' && stderr != ''
|
404
|
+
raise FileTransporterFailed, "[#{self.class}] Upload failed " \
|
405
|
+
"(exitcode: 0), but stderr present\n#{stderr}"
|
406
|
+
end
|
407
|
+
|
408
|
+
logger.debug 'Parsing CSV Response'
|
409
|
+
logger.debug output.stdout
|
410
|
+
|
411
|
+
array = CSV.parse(output.stdout, headers: true).map(&:to_hash)
|
412
|
+
array.each { |h| h.each { |key, value| h[key] = nil if value == '' } }
|
413
|
+
Hash[array.map { |entry| [entry.fetch('src_sha1'), entry] }]
|
414
|
+
end
|
415
|
+
|
416
|
+
# Converts a Ruby hash into a PowerShell hash table, represented in a
|
417
|
+
# String.
|
418
|
+
#
|
419
|
+
# @param obj [Object] source Hash or object when used in recursive
|
420
|
+
# calls
|
421
|
+
# @param depth [Integer] padding depth, used in recursive calls
|
422
|
+
# (default: `0`)
|
423
|
+
# @return [String] a PowerShell hash table
|
424
|
+
# @api private
|
425
|
+
def ps_hash(obj, depth = 0)
|
426
|
+
if obj.is_a?(Hash)
|
427
|
+
obj.map do |k, v|
|
428
|
+
%(#{pad(depth + 2)}#{ps_hash(k)} = #{ps_hash(v, depth + 2)})
|
429
|
+
end.join(";\n").insert(0, "@{\n").insert(-1, "\n#{pad(depth)}}")
|
430
|
+
else
|
431
|
+
%("#{obj}")
|
432
|
+
end
|
433
|
+
end
|
434
|
+
|
435
|
+
# Uploads an IO stream to a Base64-encoded destination file.
|
436
|
+
#
|
437
|
+
# **Implementation Note:** Some of the code in this method may appear
|
438
|
+
# slightly too dense and while adding additional variables would help,
|
439
|
+
# the code is written very precisely to avoid unwanted allocations
|
440
|
+
# which will bloat the Ruby VM's object space (and memory footprint).
|
441
|
+
# The goal here is to stream potentially large files to a remote host
|
442
|
+
# while not loading the entire file into memory first, then Base64
|
443
|
+
# encoding it--duplicating the file in memory again.
|
444
|
+
#
|
445
|
+
# @param input_io [#read] a readable stream or object to be uploaded
|
446
|
+
# @param dest [String] path to the destination file on the remote host
|
447
|
+
# @return [Integer,Integer] the number of resulting upload chunks and
|
448
|
+
# the number of bytes transferred to the remote host
|
449
|
+
# @api private
|
450
|
+
def stream_upload(input_io, dest)
|
451
|
+
read_size = ((max_encoded_write - dest.length) / 4) * 3
|
452
|
+
chunk = 1
|
453
|
+
bytes = 0
|
454
|
+
# Do not freeze this string
|
455
|
+
buffer = ''
|
456
|
+
shell.run(<<-PS
|
457
|
+
$to = $ExecutionContext.SessionState.Path.GetUnresolvedProviderPathFromPSPath("#{dest}")
|
458
|
+
$parent = Split-Path $to
|
459
|
+
if(!(Test-path $parent)) { mkdir $parent | Out-Null }
|
460
|
+
$fileStream = New-Object -TypeName System.IO.FileStream -ArgumentList @(
|
461
|
+
$to,
|
462
|
+
[system.io.filemode]::Create,
|
463
|
+
[System.io.FileAccess]::Write,
|
464
|
+
[System.IO.FileShare]::ReadWrite
|
465
|
+
)
|
466
|
+
# Powershell caches ScrpitBlocks in a dictionary
|
467
|
+
# keyed on the script block text. Thats just great
|
468
|
+
# unless the script is super large and called a gillion
|
469
|
+
# times like we might do. In such a case it will saturate the
|
470
|
+
# Large Object Heap and lead to Out Of Memory exceptions
|
471
|
+
# for large files or folders. So we call the internal method
|
472
|
+
# ClearScriptBlockCache to clear it.
|
473
|
+
$bindingFlags= [Reflection.BindingFlags] "NonPublic,Static"
|
474
|
+
$method = [scriptblock].GetMethod("ClearScriptBlockCache", $bindingFlags)
|
475
|
+
PS
|
476
|
+
)
|
477
|
+
|
478
|
+
while input_io.read(read_size, buffer)
|
479
|
+
bytes += (buffer.bytesize / 3 * 4)
|
480
|
+
shell.run(stream_command([buffer].pack(BASE64_PACK)))
|
481
|
+
logger.debug "Wrote chunk #{chunk} for #{dest}" if chunk % 25 == 0
|
482
|
+
chunk += 1
|
483
|
+
yield bytes if block_given?
|
484
|
+
end
|
485
|
+
shell.run('$fileStream.Dispose()')
|
486
|
+
buffer = nil # rubocop:disable Lint/UselessAssignment
|
487
|
+
|
488
|
+
[chunk - 1, bytes]
|
489
|
+
end
|
490
|
+
|
491
|
+
def stream_command(encoded_bytes)
|
492
|
+
<<-PS
|
493
|
+
if($method) { $method.Invoke($Null, $Null) }
|
494
|
+
$bytes=[Convert]::FromBase64String('#{encoded_bytes}')
|
495
|
+
$fileStream.Write($bytes, 0, $bytes.length)
|
496
|
+
PS
|
497
|
+
end
|
498
|
+
|
499
|
+
# Uploads a local file.
|
500
|
+
#
|
501
|
+
# @param src [String, StringIO] path to a local file or StringIO object
|
502
|
+
# @param dest [String] path to the file on the remote host
|
503
|
+
# @return [Integer,Integer] the number of resulting upload chunks and
|
504
|
+
# the number of bytes transferred to the remote host
|
505
|
+
# @api private
|
506
|
+
def stream_upload_file(src, dest, &block)
|
507
|
+
logger.debug "Uploading #{src} to #{dest}"
|
508
|
+
chunks = 0
|
509
|
+
bytes = 0
|
510
|
+
elapsed = Benchmark.measure do
|
511
|
+
if src.is_a?(StringIO)
|
512
|
+
chunks, bytes = stream_upload(src, dest, &block)
|
513
|
+
else
|
514
|
+
File.open(src, 'rb') do |io|
|
515
|
+
chunks, bytes = stream_upload(io, dest, &block)
|
516
|
+
end
|
517
|
+
end
|
518
|
+
end
|
519
|
+
logger.debug(
|
520
|
+
"Finished uploading #{src} to #{dest} " \
|
521
|
+
"(#{bytes.to_f / 1000} KB over #{chunks} chunks) " \
|
522
|
+
"in #{duration(elapsed.real)}"
|
523
|
+
)
|
524
|
+
|
525
|
+
[chunks, bytes]
|
526
|
+
end
|
527
|
+
|
528
|
+
# Uploads a collection of "dirty" files to the remote host as
|
529
|
+
# Base64-encoded temporary files. A "dirty" file is one which has the
|
530
|
+
# `"chk_dirty"` option set to `"True"` in the incoming files Hash.
|
531
|
+
#
|
532
|
+
# @param files [Hash] files hash, keyed by the local SHA1 digest
|
533
|
+
# @return [Hash] a report hash, keyed by the local SHA1 digest
|
534
|
+
# @api private
|
535
|
+
def stream_upload_files(files)
|
536
|
+
response = {}
|
537
|
+
files.each do |sha1, data|
|
538
|
+
src = data.fetch('src_zip', data['src'])
|
539
|
+
if data['chk_dirty'] == 'True'
|
540
|
+
response[sha1] = { 'dest' => data['tmpzip'] || data['dst'] }
|
541
|
+
chunks, bytes = stream_upload_file(src, data['tmpzip'] || data['dst']) do |xfered|
|
542
|
+
yield data['src'], xfered
|
543
|
+
end
|
544
|
+
response[sha1]['chunks'] = chunks
|
545
|
+
response[sha1]['xfered'] = bytes
|
546
|
+
else
|
547
|
+
logger.debug "File #{data['dst']} is up to date, skipping"
|
548
|
+
end
|
549
|
+
end
|
550
|
+
response
|
551
|
+
end
|
552
|
+
|
553
|
+
# Total by byte count to be transferred.
|
554
|
+
# Calculates count based on the sum of base64 encoded content size
|
555
|
+
# of all files base 64 that are dirty.
|
556
|
+
#
|
557
|
+
# @param files [Hash] files hash, keyed by the local SHA1 digest
|
558
|
+
# @return [Fixnum] total byte size
|
559
|
+
# @api private
|
560
|
+
def total_base64_transfer_size(files)
|
561
|
+
size = 0
|
562
|
+
files.values.each { |file| size += file['size'] if file['chk_dirty'] == 'True' }
|
563
|
+
size / 3 * 4
|
564
|
+
end
|
565
|
+
end
|
566
|
+
# rubocop:enable Metrics/MethodLength, Metrics/AbcSize, Metrics/ClassLength
|
567
|
+
end
|
568
|
+
end
|
569
|
+
end
|