winrm-fs 1.0.1 → 1.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,166 +1,177 @@
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
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 = clean_dirname(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 [Pathname] the pathname object representing dirname that
102
+ # doesn't have any of those ~ in it
103
+ # @api private
104
+ def clean_dirname(dir)
105
+ paths = Pathname.glob(dir)
106
+ if paths.length != 1
107
+ fail "Expected Pathname.glob(dir) to return only dir, got #{paths}"
108
+ end
109
+ paths.first
110
+ end
111
+
112
+ # @return [Array<Pathname] all recursive files under the base
113
+ # directory, excluding directories
114
+ # @api private
115
+ def entries
116
+ Pathname.glob(dir.join('**/*')).delete_if(&:directory?).sort
117
+ end
118
+
119
+ # (see Logging.log_subject)
120
+ # @api private
121
+ def log_subject
122
+ @log_subject ||= [self.class.to_s.split('::').last, path].join('::')
123
+ end
124
+
125
+ # Adds all file entries to the Zip output stream.
126
+ #
127
+ # @param zos [Zip::OutputStream] zip output stream
128
+ # @api private
129
+ def produce_zip_entries(zos)
130
+ entries.each do |entry|
131
+ entry_path = entry.sub(/#{dir}\//i, '')
132
+ logger.debug "+++ Adding #{entry_path}"
133
+ zos.put_next_entry(
134
+ zip_entry(entry_path),
135
+ nil, nil, ::Zip::Entry::DEFLATED, Zlib::BEST_COMPRESSION)
136
+ entry.open('rb') { |src| IO.copy_stream(src, zos) }
137
+ end
138
+ logger.debug '=== All files added.'
139
+ end
140
+
141
+ # Writes out a temporary Zip file.
142
+ #
143
+ # @api private
144
+ def write_zip
145
+ logger.debug 'Populating files'
146
+ Zip::OutputStream.write_buffer(NoDupIO.new(zip_io)) do |zos|
147
+ produce_zip_entries(zos)
148
+ end
149
+ end
150
+
151
+ def zip_entry(entry_path)
152
+ Zip::Entry.new(
153
+ zip_io.path,
154
+ entry_path.to_s,
155
+ nil, nil, nil, nil, nil, nil,
156
+ ::Zip::DOSTime.new(2000)
157
+ )
158
+ end
159
+
160
+ # Simple delegate wrapper to prevent `#dup` calls being made on IO
161
+ # objects. This is used to bypass an issue in the `Zip::Outputstream`
162
+ # constructor where an incoming IO is duplicated, leading to races
163
+ # on flushing the final stream to disk.
164
+ #
165
+ # @author Fletcher Nichol <fnichol@nichol.ca>
166
+ # @api private
167
+ class NoDupIO < SimpleDelegator
168
+ # @return [self] returns self and does *not* return a duplicate
169
+ # object
170
+ def dup
171
+ self
172
+ end
173
+ end
174
+ end
175
+ end
176
+ end
177
+ end
@@ -1,28 +1,28 @@
1
- # encoding: UTF-8
2
- #
3
- # Copyright 2015 Shawn Neal <sneal@sneal.net>
4
- #
5
- # Licensed under the Apache License, Version 2.0 (the "License");
6
- # you may not use this file except in compliance with the License.
7
- # You may obtain a copy of the License at
8
- #
9
- # http://www.apache.org/licenses/LICENSE-2.0
10
- #
11
- # Unless required by applicable law or agreed to in writing, software
12
- # distributed under the License is distributed on an "AS IS" BASIS,
13
- # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
- # See the License for the specific language governing permissions and
15
- # limitations under the License.
16
-
17
- module WinRM
18
- module FS
19
- # WinRM-FS base class for errors
20
- class WinRMFSError < StandardError; end
21
-
22
- # Error that occurs when a file upload fails
23
- class WinRMUploadError < WinRMFSError; end
24
-
25
- # Error that occurs when a file download fails
26
- class WinRMDownloadError < WinRMFSError; end
27
- end
28
- end
1
+ # encoding: UTF-8
2
+ #
3
+ # Copyright 2015 Shawn Neal <sneal@sneal.net>
4
+ #
5
+ # Licensed under the Apache License, Version 2.0 (the "License");
6
+ # you may not use this file except in compliance with the License.
7
+ # You may obtain a copy of the License at
8
+ #
9
+ # http://www.apache.org/licenses/LICENSE-2.0
10
+ #
11
+ # Unless required by applicable law or agreed to in writing, software
12
+ # distributed under the License is distributed on an "AS IS" BASIS,
13
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ # See the License for the specific language governing permissions and
15
+ # limitations under the License.
16
+
17
+ module WinRM
18
+ module FS
19
+ # WinRM-FS base class for errors
20
+ class WinRMFSError < StandardError; end
21
+
22
+ # Error that occurs when a file upload fails
23
+ class WinRMUploadError < WinRMFSError; end
24
+
25
+ # Error that occurs when a file download fails
26
+ class WinRMDownloadError < WinRMFSError; end
27
+ end
28
+ end
@@ -1,117 +1,118 @@
1
- # encoding: UTF-8
2
- #
3
- # Copyright 2015 Shawn Neal <sneal@sneal.net>
4
- #
5
- # Licensed under the Apache License, Version 2.0 (the "License");
6
- # you may not use this file except in compliance with the License.
7
- # You may obtain a copy of the License at
8
- #
9
- # http://www.apache.org/licenses/LICENSE-2.0
10
- #
11
- # Unless required by applicable law or agreed to in writing, software
12
- # distributed under the License is distributed on an "AS IS" BASIS,
13
- # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
- # See the License for the specific language governing permissions and
15
- # limitations under the License.
16
-
17
- require 'winrm'
18
- require_relative 'scripts/scripts'
19
- require_relative 'core/file_transporter'
20
-
21
- module WinRM
22
- module FS
23
- # Perform file transfer operations between a local machine and winrm endpoint
24
- class FileManager
25
- # Creates a new FileManager instance
26
- # @param [WinRM::Connection] WinRM web connection client
27
- def initialize(connection)
28
- @connection = connection
29
- @logger = connection.logger
30
- end
31
-
32
- # Gets the MD5 checksum of the specified file if it exists,
33
- # otherwise ''
34
- # @param [String] The remote file path
35
- def checksum(path)
36
- @logger.debug("checksum: #{path}")
37
- script = WinRM::FS::Scripts.render('checksum', path: path)
38
- @connection.shell(:powershell) { |e| e.run(script).stdout.chomp }
39
- end
40
-
41
- # Create the specifed directory recursively
42
- # @param [String] The remote dir to create
43
- # @return [Boolean] True if successful, otherwise false
44
- def create_dir(path)
45
- @logger.debug("create_dir: #{path}")
46
- script = WinRM::FS::Scripts.render('create_dir', path: path)
47
- @connection.shell(:powershell) { |e| e.run(script).exitcode == 0 }
48
- end
49
-
50
- # Deletes the file or directory at the specified path
51
- # @param [String] The path to remove
52
- # @return [Boolean] True if successful, otherwise False
53
- def delete(path)
54
- @logger.debug("deleting: #{path}")
55
- script = WinRM::FS::Scripts.render('delete', path: path)
56
- @connection.shell(:powershell) { |e| e.run(script).exitcode == 0 }
57
- end
58
-
59
- # Downloads the specified remote file to the specified local path
60
- # @param [String] The full path on the remote machine
61
- # @param [String] The full path to write the file to locally
62
- def download(remote_path, local_path)
63
- @logger.debug("downloading: #{remote_path} -> #{local_path}")
64
- script = WinRM::FS::Scripts.render('download', path: remote_path)
65
- output = @connection.shell(:powershell) { |e| e.run(script) }
66
- return false if output.exitcode != 0
67
- contents = output.stdout.gsub('\n\r', '')
68
- out = Base64.decode64(contents)
69
- IO.binwrite(local_path, out)
70
- true
71
- end
72
-
73
- # Checks to see if the given path exists on the target file system.
74
- # @param [String] The full path to the directory or file
75
- # @return [Boolean] True if the file/dir exists, otherwise false.
76
- def exists?(path)
77
- @logger.debug("exists?: #{path}")
78
- script = WinRM::FS::Scripts.render('exists', path: path)
79
- @connection.shell(:powershell) { |e| e.run(script).exitcode == 0 }
80
- end
81
-
82
- # Gets the current user's TEMP directory on the remote system, for example
83
- # 'C:/Windows/Temp'
84
- # @return [String] Full path to the temp directory
85
- def temp_dir
86
- @guest_temp ||= begin
87
- (@connection.shell(:powershell) { |e| e.run('$env:TEMP') }).stdout.chomp.gsub('\\', '/')
88
- end
89
- end
90
-
91
- # Upload one or more local files and directories to a remote directory
92
- # @example copy a single file to a winrm endpoint
93
- #
94
- # file_manager.upload('/Users/sneal/myfile.txt', 'c:/foo/myfile.txt')
95
- #
96
- # @example copy a single directory to a winrm endpoint
97
- #
98
- # file_manager.upload('c:/dev/my_dir', '$env:AppData')
99
- #
100
- # @param [String] A path to a local directory or file that will be copied
101
- # to the remote Windows box.
102
- # @param [String] The target directory or file
103
- # This path may contain powershell style environment variables
104
- # @yieldparam [Fixnum] Number of bytes copied in current payload sent to the winrm endpoint
105
- # @yieldparam [Fixnum] The total number of bytes to be copied
106
- # @yieldparam [String] Path of file being copied
107
- # @yieldparam [String] Target path on the winrm endpoint
108
- # @return [Fixnum] The total number of bytes copied
109
- def upload(local_path, remote_path, &block)
110
- @connection.shell(:powershell) do |shell|
111
- file_transporter ||= WinRM::FS::Core::FileTransporter.new(shell)
112
- file_transporter.upload(local_path, remote_path, &block)[0]
113
- end
114
- end
115
- end
116
- end
117
- end
1
+ # encoding: UTF-8
2
+ #
3
+ # Copyright 2015 Shawn Neal <sneal@sneal.net>
4
+ #
5
+ # Licensed under the Apache License, Version 2.0 (the "License");
6
+ # you may not use this file except in compliance with the License.
7
+ # You may obtain a copy of the License at
8
+ #
9
+ # http://www.apache.org/licenses/LICENSE-2.0
10
+ #
11
+ # Unless required by applicable law or agreed to in writing, software
12
+ # distributed under the License is distributed on an "AS IS" BASIS,
13
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ # See the License for the specific language governing permissions and
15
+ # limitations under the License.
16
+
17
+ require 'winrm'
18
+ require_relative 'scripts/scripts'
19
+ require_relative 'core/file_transporter'
20
+
21
+ module WinRM
22
+ module FS
23
+ # Perform file transfer operations between a local machine and winrm endpoint
24
+ class FileManager
25
+ # Creates a new FileManager instance
26
+ # @param [WinRM::Connection] WinRM web connection client
27
+ def initialize(connection)
28
+ @connection = connection
29
+ @logger = connection.logger
30
+ end
31
+
32
+ # Gets the MD5 checksum of the specified file if it exists,
33
+ # otherwise ''
34
+ # @param [String] The remote file path
35
+ # @parms [String] The digest method
36
+ def checksum(path, digest = 'MD5')
37
+ @logger.debug("checksum with #{digest}: #{path}")
38
+ script = WinRM::FS::Scripts.render('checksum', path: path, digest: digest)
39
+ @connection.shell(:powershell) { |e| e.run(script).stdout.chomp }
40
+ end
41
+
42
+ # Create the specifed directory recursively
43
+ # @param [String] The remote dir to create
44
+ # @return [Boolean] True if successful, otherwise false
45
+ def create_dir(path)
46
+ @logger.debug("create_dir: #{path}")
47
+ script = WinRM::FS::Scripts.render('create_dir', path: path)
48
+ @connection.shell(:powershell) { |e| e.run(script).exitcode == 0 }
49
+ end
50
+
51
+ # Deletes the file or directory at the specified path
52
+ # @param [String] The path to remove
53
+ # @return [Boolean] True if successful, otherwise False
54
+ def delete(path)
55
+ @logger.debug("deleting: #{path}")
56
+ script = WinRM::FS::Scripts.render('delete', path: path)
57
+ @connection.shell(:powershell) { |e| e.run(script).exitcode == 0 }
58
+ end
59
+
60
+ # Downloads the specified remote file to the specified local path
61
+ # @param [String] The full path on the remote machine
62
+ # @param [String] The full path to write the file to locally
63
+ def download(remote_path, local_path)
64
+ @logger.debug("downloading: #{remote_path} -> #{local_path}")
65
+ script = WinRM::FS::Scripts.render('download', path: remote_path)
66
+ output = @connection.shell(:powershell) { |e| e.run(script) }
67
+ return false if output.exitcode != 0
68
+ contents = output.stdout.gsub('\n\r', '')
69
+ out = Base64.decode64(contents)
70
+ IO.binwrite(local_path, out)
71
+ true
72
+ end
73
+
74
+ # Checks to see if the given path exists on the target file system.
75
+ # @param [String] The full path to the directory or file
76
+ # @return [Boolean] True if the file/dir exists, otherwise false.
77
+ def exists?(path)
78
+ @logger.debug("exists?: #{path}")
79
+ script = WinRM::FS::Scripts.render('exists', path: path)
80
+ @connection.shell(:powershell) { |e| e.run(script).exitcode == 0 }
81
+ end
82
+
83
+ # Gets the current user's TEMP directory on the remote system, for example
84
+ # 'C:/Windows/Temp'
85
+ # @return [String] Full path to the temp directory
86
+ def temp_dir
87
+ @guest_temp ||= begin
88
+ (@connection.shell(:powershell) { |e| e.run('$env:TEMP') }).stdout.chomp.gsub('\\', '/')
89
+ end
90
+ end
91
+
92
+ # Upload one or more local files and directories to a remote directory
93
+ # @example copy a single file to a winrm endpoint
94
+ #
95
+ # file_manager.upload('/Users/sneal/myfile.txt', 'c:/foo/myfile.txt')
96
+ #
97
+ # @example copy a single directory to a winrm endpoint
98
+ #
99
+ # file_manager.upload('c:/dev/my_dir', '$env:AppData')
100
+ #
101
+ # @param [String] A path to a local directory or file that will be copied
102
+ # to the remote Windows box.
103
+ # @param [String] The target directory or file
104
+ # This path may contain powershell style environment variables
105
+ # @yieldparam [Fixnum] Number of bytes copied in current payload sent to the winrm endpoint
106
+ # @yieldparam [Fixnum] The total number of bytes to be copied
107
+ # @yieldparam [String] Path of file being copied
108
+ # @yieldparam [String] Target path on the winrm endpoint
109
+ # @return [Fixnum] The total number of bytes copied
110
+ def upload(local_path, remote_path, &block)
111
+ @connection.shell(:powershell) do |shell|
112
+ file_transporter ||= WinRM::FS::Core::FileTransporter.new(shell)
113
+ file_transporter.upload(local_path, remote_path, &block)[0]
114
+ end
115
+ end
116
+ end
117
+ end
118
+ end