bolt 2.19.0 → 2.20.0
Sign up to get free protection for your applications and to get access to all the features.
Potentially problematic release.
This version of bolt might be problematic. Click here for more details.
- checksums.yaml +4 -4
- data/bolt-modules/boltlib/lib/puppet/functions/download_file.rb +123 -0
- data/lib/bolt/bolt_option_parser.rb +25 -2
- data/lib/bolt/catalog.rb +3 -2
- data/lib/bolt/cli.rb +139 -92
- data/lib/bolt/config.rb +1 -1
- data/lib/bolt/config/options.rb +14 -0
- data/lib/bolt/executor.rb +15 -0
- data/lib/bolt/inventory/group.rb +3 -2
- data/lib/bolt/inventory/inventory.rb +4 -3
- data/lib/bolt/outputter/rainbow.rb +3 -2
- data/lib/bolt/pal.rb +8 -2
- data/lib/bolt/pal/yaml_plan/evaluator.rb +18 -1
- data/lib/bolt/pal/yaml_plan/step.rb +11 -2
- data/lib/bolt/pal/yaml_plan/step/download.rb +38 -0
- data/lib/bolt/pal/yaml_plan/step/upload.rb +3 -3
- data/lib/bolt/plugin/puppetdb.rb +3 -2
- data/lib/bolt/project.rb +2 -1
- data/lib/bolt/puppetdb/client.rb +2 -0
- data/lib/bolt/puppetdb/config.rb +16 -0
- data/lib/bolt/result.rb +7 -0
- data/lib/bolt/shell/bash.rb +24 -4
- data/lib/bolt/shell/powershell.rb +10 -4
- data/lib/bolt/transport/base.rb +24 -0
- data/lib/bolt/transport/docker.rb +8 -0
- data/lib/bolt/transport/docker/connection.rb +20 -2
- data/lib/bolt/transport/local/connection.rb +14 -1
- data/lib/bolt/transport/orch.rb +12 -0
- data/lib/bolt/transport/simple.rb +6 -0
- data/lib/bolt/transport/ssh/connection.rb +9 -1
- data/lib/bolt/transport/ssh/exec_connection.rb +22 -1
- data/lib/bolt/transport/winrm/connection.rb +109 -8
- data/lib/bolt/util.rb +26 -11
- data/lib/bolt/version.rb +1 -1
- data/lib/bolt_server/transport_app.rb +3 -2
- data/lib/bolt_spec/bolt_context.rb +7 -2
- data/lib/bolt_spec/plans.rb +15 -2
- data/lib/bolt_spec/plans/action_stubs.rb +2 -1
- data/lib/bolt_spec/plans/action_stubs/download_stub.rb +66 -0
- data/lib/bolt_spec/plans/mock_executor.rb +14 -1
- data/lib/bolt_spec/run.rb +22 -0
- data/libexec/bolt_catalog +3 -2
- metadata +5 -2
data/lib/bolt/transport/base.rb
CHANGED
@@ -167,6 +167,25 @@ module Bolt
|
|
167
167
|
end
|
168
168
|
end
|
169
169
|
|
170
|
+
# Downloads the given source file from a batch of targets to the destination location
|
171
|
+
# on the host.
|
172
|
+
#
|
173
|
+
# The default implementation only supports batches of size 1 and will fail otherwise.
|
174
|
+
#
|
175
|
+
# Transports may override this method to implement their own batch processing.
|
176
|
+
def batch_download(targets, source, destination, options = {}, &callback)
|
177
|
+
require 'erb'
|
178
|
+
|
179
|
+
assert_batch_size_one("batch_download()", targets)
|
180
|
+
target = targets.first
|
181
|
+
with_events(target, callback, 'download') do
|
182
|
+
escaped_name = ERB::Util.url_encode(target.safe_name)
|
183
|
+
target_destination = File.expand_path(escaped_name, destination)
|
184
|
+
@logger.debug { "Downloading: '#{source}' on #{target.safe_name} to #{target_destination}" }
|
185
|
+
download(target, source, target_destination, options)
|
186
|
+
end
|
187
|
+
end
|
188
|
+
|
170
189
|
def batch_connected?(targets)
|
171
190
|
assert_batch_size_one("connected?()", targets)
|
172
191
|
connected?(targets.first)
|
@@ -201,6 +220,11 @@ module Bolt
|
|
201
220
|
raise NotImplementedError, "upload() must be implemented by the transport class"
|
202
221
|
end
|
203
222
|
|
223
|
+
# Transports should override this method with their own implementation of file download.
|
224
|
+
def download(*_args)
|
225
|
+
raise NotImplementedError, "download() must be implemented by the transport class"
|
226
|
+
end
|
227
|
+
|
204
228
|
# Transports should override this method with their own implementation of a connection test.
|
205
229
|
def connected?(_targets)
|
206
230
|
raise NotImplementedError, "connected?() must be implemented by the transport class"
|
@@ -38,6 +38,14 @@ module Bolt
|
|
38
38
|
end
|
39
39
|
end
|
40
40
|
|
41
|
+
def download(target, source, destination, _options = {})
|
42
|
+
with_connection(target) do |conn|
|
43
|
+
download = File.join(destination, Bolt::Util.unix_basename(source))
|
44
|
+
conn.download_remote_content(source, destination)
|
45
|
+
Bolt::Result.for_download(target, source, destination, download)
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
41
49
|
def run_command(target, command, options = {})
|
42
50
|
execute_options = {}
|
43
51
|
execute_options[:tty] = target.options['tty']
|
@@ -82,7 +82,9 @@ module Bolt
|
|
82
82
|
def write_remote_file(source, destination)
|
83
83
|
@logger.debug { "Uploading #{source}, to #{destination}" }
|
84
84
|
_, stdout_str, status = execute_local_docker_command('cp', [source, "#{container_id}:#{destination}"])
|
85
|
-
|
85
|
+
unless status.exitstatus.zero?
|
86
|
+
raise "Error writing file to container #{@container_id}: #{stdout_str}"
|
87
|
+
end
|
86
88
|
rescue StandardError => e
|
87
89
|
raise Bolt::Node::FileError.new(e.message, 'WRITE_ERROR')
|
88
90
|
end
|
@@ -90,7 +92,23 @@ module Bolt
|
|
90
92
|
def write_remote_directory(source, destination)
|
91
93
|
@logger.debug { "Uploading #{source}, to #{destination}" }
|
92
94
|
_, stdout_str, status = execute_local_docker_command('cp', [source, "#{container_id}:#{destination}"])
|
93
|
-
|
95
|
+
unless status.exitstatus.zero?
|
96
|
+
raise "Error writing directory to container #{@container_id}: #{stdout_str}"
|
97
|
+
end
|
98
|
+
rescue StandardError => e
|
99
|
+
raise Bolt::Node::FileError.new(e.message, 'WRITE_ERROR')
|
100
|
+
end
|
101
|
+
|
102
|
+
def download_remote_content(source, destination)
|
103
|
+
@logger.debug { "Downloading #{source} to #{destination}" }
|
104
|
+
# Create the destination directory, otherwise copying a source directory with Docker will
|
105
|
+
# copy the *contents* of the directory.
|
106
|
+
# https://docs.docker.com/engine/reference/commandline/cp/
|
107
|
+
FileUtils.mkdir_p(destination)
|
108
|
+
_, stdout_str, status = execute_local_docker_command('cp', ["#{container_id}:#{source}", destination])
|
109
|
+
unless status.exitstatus.zero?
|
110
|
+
raise "Error downloading content from container #{@container_id}: #{stdout_str}"
|
111
|
+
end
|
94
112
|
rescue StandardError => e
|
95
113
|
raise Bolt::Node::FileError.new(e.message, 'WRITE_ERROR')
|
96
114
|
end
|
@@ -27,7 +27,7 @@ module Bolt
|
|
27
27
|
end
|
28
28
|
end
|
29
29
|
|
30
|
-
def
|
30
|
+
def upload_file(source, dest)
|
31
31
|
@logger.debug { "Uploading #{source}, to #{dest}" }
|
32
32
|
if source.is_a?(StringIO)
|
33
33
|
Tempfile.create(File.basename(dest)) do |f|
|
@@ -45,6 +45,19 @@ module Bolt
|
|
45
45
|
raise Bolt::Node::FileError.new(message, 'COPY_ERROR')
|
46
46
|
end
|
47
47
|
|
48
|
+
def download_file(source, dest, _download)
|
49
|
+
@logger.debug { "Downloading #{source} to #{dest}" }
|
50
|
+
# Create the destination directory for the target, or the
|
51
|
+
# copied file will have the target's name
|
52
|
+
FileUtils.mkdir_p(dest)
|
53
|
+
# Mimic the behavior of `cp --remove-destination`
|
54
|
+
# since the flag isn't supported on MacOS
|
55
|
+
FileUtils.cp_r(source, dest, remove_destination: true)
|
56
|
+
rescue StandardError => e
|
57
|
+
message = "Could not download file to #{dest}: #{e}"
|
58
|
+
raise Bolt::Node::FileError.new(message, 'DOWNLOAD_ERROR')
|
59
|
+
end
|
60
|
+
|
48
61
|
def execute(command)
|
49
62
|
if Bolt::Util.windows?
|
50
63
|
# If it's already a powershell command then invoke it normally.
|
data/lib/bolt/transport/orch.rb
CHANGED
@@ -184,6 +184,18 @@ module Bolt
|
|
184
184
|
end
|
185
185
|
end
|
186
186
|
|
187
|
+
def batch_download(targets, *_args)
|
188
|
+
error = {
|
189
|
+
'kind' => 'bolt/not-supported-error',
|
190
|
+
'msg' => 'pcp transport does not support downloading files',
|
191
|
+
'details' => {}
|
192
|
+
}
|
193
|
+
|
194
|
+
targets.map do |target|
|
195
|
+
Bolt::Result.new(target, error: error, action: 'download')
|
196
|
+
end
|
197
|
+
end
|
198
|
+
|
187
199
|
def batches(targets)
|
188
200
|
targets.group_by { |target| Connection.get_key(target.options) }.values
|
189
201
|
end
|
@@ -32,6 +32,12 @@ module Bolt
|
|
32
32
|
end
|
33
33
|
end
|
34
34
|
|
35
|
+
def download(target, source, destination, options = {})
|
36
|
+
with_connection(target) do |conn|
|
37
|
+
conn.shell.download(source, destination, options)
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
35
41
|
def run_script(target, script, arguments, options = {})
|
36
42
|
with_connection(target) do |conn|
|
37
43
|
conn.shell.run_script(script, arguments, options)
|
@@ -235,7 +235,7 @@ module Bolt
|
|
235
235
|
raise Bolt::Error.new(msg, 'bolt/too-many-files')
|
236
236
|
end
|
237
237
|
|
238
|
-
def
|
238
|
+
def upload_file(source, destination)
|
239
239
|
# Do not log wrapper script content
|
240
240
|
@logger.debug { "Uploading #{source}, to #{destination}" } unless source.is_a?(StringIO)
|
241
241
|
@session.scp.upload!(source, destination, recursive: true)
|
@@ -243,6 +243,14 @@ module Bolt
|
|
243
243
|
raise Bolt::Node::FileError.new(e.message, 'WRITE_ERROR')
|
244
244
|
end
|
245
245
|
|
246
|
+
def download_file(source, destination, _download)
|
247
|
+
# Do not log wrapper script content
|
248
|
+
@logger.debug { "Downloading #{source} to #{destination}" }
|
249
|
+
@session.scp.download!(source, destination, recursive: true)
|
250
|
+
rescue StandardError => e
|
251
|
+
raise Bolt::Node::FileError.new(e.message, 'WRITE_ERROR')
|
252
|
+
end
|
253
|
+
|
246
254
|
# This handles renaming Net::SSH verifiers between version 4.x and 5.x
|
247
255
|
# of the gem
|
248
256
|
def net_ssh_verifier(verifier)
|
@@ -65,7 +65,7 @@ module Bolt
|
|
65
65
|
ssh_cmd << command
|
66
66
|
end
|
67
67
|
|
68
|
-
def
|
68
|
+
def upload_file(source, dest)
|
69
69
|
@logger.debug { "Uploading #{source}, to #{userhost}:#{dest}" } unless source.is_a?(StringIO)
|
70
70
|
|
71
71
|
cp_conf = @target.transport_config['copy-command'] || ["scp", "-r"]
|
@@ -94,6 +94,27 @@ module Bolt
|
|
94
94
|
end
|
95
95
|
end
|
96
96
|
|
97
|
+
def download_file(source, dest, _download)
|
98
|
+
@logger.debug { "Downloading #{userhost}:#{source} to #{dest}" }
|
99
|
+
|
100
|
+
FileUtils.mkdir_p(dest)
|
101
|
+
|
102
|
+
cp_conf = @target.transport_config['copy-command'] || ["scp", "-r"]
|
103
|
+
cp_cmd = Array(cp_conf)
|
104
|
+
cp_cmd += ssh_opts
|
105
|
+
cp_cmd << "#{userhost}:#{Shellwords.escape(source)}"
|
106
|
+
cp_cmd << dest
|
107
|
+
|
108
|
+
_, err, stat = Open3.capture3(*cp_cmd)
|
109
|
+
|
110
|
+
if stat.success?
|
111
|
+
@logger.debug "Successfully downloaded #{userhost}:#{source} to #{dest}"
|
112
|
+
else
|
113
|
+
message = "Could not copy file to #{dest}: #{err}"
|
114
|
+
raise Bolt::Node::FileError.new(message, 'COPY_ERROR')
|
115
|
+
end
|
116
|
+
end
|
117
|
+
|
97
118
|
def execute(command)
|
98
119
|
cmd_array = build_ssh_command(command)
|
99
120
|
Open3.popen3(*cmd_array)
|
@@ -129,23 +129,23 @@ module Bolt
|
|
129
129
|
raise
|
130
130
|
end
|
131
131
|
|
132
|
-
def
|
132
|
+
def upload_file(source, destination)
|
133
133
|
@logger.debug { "Uploading #{source}, to #{destination}" }
|
134
134
|
if target.options['file-protocol'] == 'smb'
|
135
|
-
|
135
|
+
upload_file_smb(source, destination)
|
136
136
|
else
|
137
|
-
|
137
|
+
upload_file_winrm(source, destination)
|
138
138
|
end
|
139
139
|
end
|
140
140
|
|
141
|
-
def
|
141
|
+
def upload_file_winrm(source, destination)
|
142
142
|
fs = ::WinRM::FS::FileManager.new(@connection)
|
143
143
|
fs.upload(source, destination)
|
144
144
|
rescue StandardError => e
|
145
145
|
raise Bolt::Node::FileError.new(e.message, 'WRITE_ERROR')
|
146
146
|
end
|
147
147
|
|
148
|
-
def
|
148
|
+
def upload_file_smb(source, destination)
|
149
149
|
# lazy-load expensive gem code
|
150
150
|
require 'ruby_smb'
|
151
151
|
|
@@ -165,7 +165,7 @@ module Bolt
|
|
165
165
|
client = smb_client_login
|
166
166
|
tree = client.tree_connect(path)
|
167
167
|
begin
|
168
|
-
|
168
|
+
upload_file_smb_recursive(tree, source, dest)
|
169
169
|
ensure
|
170
170
|
tree.disconnect!
|
171
171
|
end
|
@@ -175,6 +175,61 @@ module Bolt
|
|
175
175
|
raise Bolt::Node::FileError.new(e.message, 'WRITE_ERROR')
|
176
176
|
end
|
177
177
|
|
178
|
+
def download_file(source, destination, download)
|
179
|
+
@logger.debug { "Downloading #{source} to #{destination}" }
|
180
|
+
if target.options['file-protocol'] == 'smb'
|
181
|
+
download_file_smb(source, destination)
|
182
|
+
else
|
183
|
+
download_file_winrm(source, destination, download)
|
184
|
+
end
|
185
|
+
end
|
186
|
+
|
187
|
+
def download_file_winrm(source, destination, download)
|
188
|
+
# The winrm gem doesn't create the destination directory if it's missing,
|
189
|
+
# so create it here
|
190
|
+
FileUtils.mkdir_p(destination)
|
191
|
+
fs = ::WinRM::FS::FileManager.new(@connection)
|
192
|
+
# params: source, destination, chunksize, first
|
193
|
+
# first needs to be set to false, otherwise if the source is a directory it
|
194
|
+
# will be nested inside a directory with the same name
|
195
|
+
fs.download(source, download, 1024 * 1024, false)
|
196
|
+
rescue StandardError => e
|
197
|
+
raise Bolt::Node::FileError.new(e.message, 'WRITE_ERROR')
|
198
|
+
end
|
199
|
+
|
200
|
+
def download_file_smb(source, destination)
|
201
|
+
# lazy-load expensive gem code
|
202
|
+
require 'ruby_smb'
|
203
|
+
|
204
|
+
win_source = source.tr('/', '\\')
|
205
|
+
if (md = win_source.match(/^([a-z]):\\(.*)/i))
|
206
|
+
# if drive, use admin share for that drive, so path is '\\host\C$'
|
207
|
+
path = "\\\\#{@target.host}\\#{md[1]}$"
|
208
|
+
src = md[2]
|
209
|
+
elsif (md = win_source.match(/^(\\\\[^\\]+\\[^\\]+)\\(.*)/))
|
210
|
+
# if unc, path is '\\host\share'
|
211
|
+
path = md[1]
|
212
|
+
src = md[2]
|
213
|
+
else
|
214
|
+
raise ArgumentError, "Unknown source '#{source}'"
|
215
|
+
end
|
216
|
+
|
217
|
+
client = smb_client_login
|
218
|
+
tree = client.tree_connect(path)
|
219
|
+
|
220
|
+
begin
|
221
|
+
# Make sure the root download directory for the target exists
|
222
|
+
FileUtils.mkdir_p(destination)
|
223
|
+
download_file_smb_recursive(tree, src, destination)
|
224
|
+
ensure
|
225
|
+
tree.disconnect!
|
226
|
+
end
|
227
|
+
rescue ::RubySMB::Error::UnexpectedStatusCode => e
|
228
|
+
raise Bolt::Node::FileError.new("SMB Error: #{e.message}", 'DOWNLOAD_ERROR')
|
229
|
+
rescue StandardError => e
|
230
|
+
raise Bolt::Node::FileError.new(e.message, 'DOWNLOAD_ERROR')
|
231
|
+
end
|
232
|
+
|
178
233
|
def shell
|
179
234
|
@shell ||= Bolt::Shell::Powershell.new(target, self)
|
180
235
|
end
|
@@ -230,13 +285,13 @@ module Bolt
|
|
230
285
|
)
|
231
286
|
end
|
232
287
|
|
233
|
-
def
|
288
|
+
def upload_file_smb_recursive(tree, source, dest)
|
234
289
|
if Dir.exist?(source)
|
235
290
|
tree.open_directory(directory: dest, write: true, disposition: ::RubySMB::Dispositions::FILE_OPEN_IF)
|
236
291
|
|
237
292
|
Dir.children(source).each do |child|
|
238
293
|
child_dest = dest + '\\' + child
|
239
|
-
|
294
|
+
upload_file_smb_recursive(tree, File.join(source, child), child_dest)
|
240
295
|
end
|
241
296
|
return
|
242
297
|
end
|
@@ -255,6 +310,52 @@ module Bolt
|
|
255
310
|
file.close
|
256
311
|
end
|
257
312
|
end
|
313
|
+
|
314
|
+
def download_file_smb_recursive(tree, source, destination)
|
315
|
+
dest = File.expand_path(Bolt::Util.windows_basename(source), destination)
|
316
|
+
|
317
|
+
# Check if the source is a directory by attempting to list its children.
|
318
|
+
# If the source is a directory, create the directory on the host and then
|
319
|
+
# recurse through the children.
|
320
|
+
if (children = list_directory_children_smb(tree, source))
|
321
|
+
FileUtils.mkdir_p(dest)
|
322
|
+
|
323
|
+
children.each do |child|
|
324
|
+
# File names are encoded UTF_16LE.
|
325
|
+
filename = child.file_name.encode(Encoding::UTF_8)
|
326
|
+
|
327
|
+
next if %w[. ..].include?(filename)
|
328
|
+
|
329
|
+
src = source + '\\' + filename
|
330
|
+
download_file_smb_recursive(tree, src, dest)
|
331
|
+
end
|
332
|
+
# If the source wasn't a directory and just returns 'STATUS_NOT_A_DIRECTORY, then
|
333
|
+
# it is a file. Write it to the host.
|
334
|
+
else
|
335
|
+
begin
|
336
|
+
file = tree.open_file(filename: source)
|
337
|
+
data = file.read
|
338
|
+
|
339
|
+
# Files may be encoded UTF_16LE
|
340
|
+
data = data.encode(Encoding::UTF_8) if data.encoding == Encoding::UTF_16LE
|
341
|
+
|
342
|
+
File.write(dest, data)
|
343
|
+
ensure
|
344
|
+
file.close
|
345
|
+
end
|
346
|
+
end
|
347
|
+
end
|
348
|
+
|
349
|
+
# Lists the children of a directory using rb_smb
|
350
|
+
# Returns an array of RubySMB::Fscc::FileInformation::FileIdFullDirectoryInformation objects
|
351
|
+
# if the source is a directory, or raises RubySMB::Error::UnexpectedStatusCode otherwise.
|
352
|
+
def list_directory_children_smb(tree, source)
|
353
|
+
tree.list(directory: source)
|
354
|
+
rescue RubySMB::Error::UnexpectedStatusCode => e
|
355
|
+
unless e.message == 'STATUS_NOT_A_DIRECTORY'
|
356
|
+
raise e
|
357
|
+
end
|
358
|
+
end
|
258
359
|
end
|
259
360
|
end
|
260
361
|
end
|
data/lib/bolt/util.rb
CHANGED
@@ -107,12 +107,13 @@ module Bolt
|
|
107
107
|
# Accepts a Data object and returns a copy with all hash keys
|
108
108
|
# modified by block. use &:to_s to stringify keys or &:to_sym to symbolize them
|
109
109
|
def walk_keys(data, &block)
|
110
|
-
|
110
|
+
case data
|
111
|
+
when Hash
|
111
112
|
data.each_with_object({}) do |(k, v), acc|
|
112
113
|
v = walk_keys(v, &block)
|
113
114
|
acc[yield(k)] = v
|
114
115
|
end
|
115
|
-
|
116
|
+
when Array
|
116
117
|
data.map { |v| walk_keys(v, &block) }
|
117
118
|
else
|
118
119
|
data
|
@@ -124,9 +125,10 @@ module Bolt
|
|
124
125
|
# their descendants are.
|
125
126
|
def walk_vals(data, skip_top = false, &block)
|
126
127
|
data = yield(data) unless skip_top
|
127
|
-
|
128
|
+
case data
|
129
|
+
when Hash
|
128
130
|
data.transform_values { |v| walk_vals(v, &block) }
|
129
|
-
|
131
|
+
when Array
|
130
132
|
data.map { |v| walk_vals(v, &block) }
|
131
133
|
else
|
132
134
|
data
|
@@ -137,9 +139,10 @@ module Bolt
|
|
137
139
|
# modified by the given block. Descendants are modified before their
|
138
140
|
# parents.
|
139
141
|
def postwalk_vals(data, skip_top = false, &block)
|
140
|
-
new_data =
|
142
|
+
new_data = case data
|
143
|
+
when Hash
|
141
144
|
data.transform_values { |v| postwalk_vals(v, &block) }
|
142
|
-
|
145
|
+
when Array
|
143
146
|
data.map { |v| postwalk_vals(v, &block) }
|
144
147
|
else
|
145
148
|
data
|
@@ -193,11 +196,12 @@ module Bolt
|
|
193
196
|
cloned[obj.object_id] = cl
|
194
197
|
cloned[cl.object_id] = cl
|
195
198
|
|
196
|
-
|
199
|
+
case cl
|
200
|
+
when Hash
|
197
201
|
obj.each { |k, v| cl[k] = deep_clone(v, cloned) }
|
198
|
-
|
202
|
+
when Array
|
199
203
|
cl.collect! { |v| deep_clone(v, cloned) }
|
200
|
-
|
204
|
+
when Struct
|
201
205
|
obj.each_pair { |k, v| cl[k] = deep_clone(v, cloned) }
|
202
206
|
end
|
203
207
|
|
@@ -257,14 +261,25 @@ module Bolt
|
|
257
261
|
|
258
262
|
# Recursively searches a data structure for plugin references
|
259
263
|
def references?(input)
|
260
|
-
|
264
|
+
case input
|
265
|
+
when Hash
|
261
266
|
input.key?('_plugin') || input.values.any? { |v| references?(v) }
|
262
|
-
|
267
|
+
when Array
|
263
268
|
input.any? { |v| references?(v) }
|
264
269
|
else
|
265
270
|
false
|
266
271
|
end
|
267
272
|
end
|
273
|
+
|
274
|
+
def unix_basename(path)
|
275
|
+
raise Bolt::ValidationError, "path must be a String, received #{path.class} #{path}" unless path.is_a?(String)
|
276
|
+
path.split('/').last
|
277
|
+
end
|
278
|
+
|
279
|
+
def windows_basename(path)
|
280
|
+
raise Bolt::ValidationError, "path must be a String, received #{path.class} #{path}" unless path.is_a?(String)
|
281
|
+
path.split(%r{[/\\]}).last
|
282
|
+
end
|
268
283
|
end
|
269
284
|
end
|
270
285
|
end
|