winrm-fs 1.0.1 → 1.0.2

Sign up to get free protection for your applications and to get access to all the features.
@@ -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